hamlet-plugin-template 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -5,9 +5,11 @@ Base template for creating Hamlet plugins.
5
5
  ## Usage
6
6
 
7
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.
8
+ 2. Add your Handlebars partials as `.hbs` (or `.xml`, `.handlebars`) files inside `src/`.
9
+ 3. Implement helpers and set the namespace in `index.js`.
10
+ 4. Run `npm run build:partials` to generate `lib/partials.js` from your source files.
11
+ 5. Run the tests.
12
+ 6. Publish to npm.
11
13
 
12
14
  ## End-user installation
13
15
 
@@ -17,28 +19,46 @@ npm install hamlet-plugin-template
17
19
 
18
20
  ```js
19
21
  // hamlet.config.js
20
- import myPlugin from 'hamlet-plugin-template'
22
+ import Sample from 'hamlet-plugin-template'
21
23
 
22
24
  export default {
23
25
  plugins: [
24
- myPlugin(),
26
+ Sample(),
25
27
  // or with options:
26
- myPlugin({ myOption: true }),
28
+ Sample({ myOption: true }),
27
29
  ],
28
30
  }
29
31
  ```
30
32
 
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.
33
+ 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.
34
+
35
+ ## Project structure
36
+
37
+ ```
38
+ ├── src/
39
+ │ ├── card.hbs ← your partial source files
40
+ │ └── hello.hbs
41
+ ├── lib/
42
+ │ └── partials.js ← auto-generated, do not edit
43
+ ├── scripts/
44
+ │ └── build-partials.js
45
+ ├── index.js
46
+ └── package.json
47
+ ```
48
+
49
+ Source files in `src/` are compiled into `lib/partials.js` by `npm run build:partials`. This script runs automatically before publishing via the `prepublishOnly` hook.
32
50
 
33
51
  ## Plugin contract
34
52
 
35
53
  A plugin must export a default factory function that receives the user's options and returns an object with the following shape:
36
54
 
37
55
  ```js
56
+ import { partials } from './lib/partials.js'
57
+
38
58
  export default function hamletPlugin(options = {}) {
39
59
  return {
40
- namespace: 'MyPlugin',
41
- partials: {},
60
+ namespace: 'Sample',
61
+ partials,
42
62
  helpers: {},
43
63
  }
44
64
  }
@@ -48,75 +68,90 @@ export default function hamletPlugin(options = {}) {
48
68
 
49
69
  Every plugin must define a unique `namespace`.
50
70
 
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.
71
+ - It must start with a letter.
72
+ - It may contain only letters and digits.
73
+ - It is used to prefix all exported partials and helpers automatically you only use short names inside the plugin.
74
+ - The namespace `hamlet` is reserved and cannot be used.
54
75
 
55
76
  ### partials
56
77
 
57
- An object containing Handlebars partials.
78
+ An object of Handlebars partials keyed by **short names** (without the namespace prefix). Hamlet registers them as `<namespace>.<name>` automatically.
58
79
 
59
- Every partial name must be prefixed with `<namespace>.`.
80
+ Add `.hbs`, `.xml`, or `.handlebars` files to `src/` and run `npm run build:partials` to populate `lib/partials.js`. The file name becomes the partial key.
60
81
 
61
- Example:
82
+ Example — `src/card.hbs`:
62
83
 
63
- ```js
64
- export default function hamletPlugin(options = {}) {
65
- return {
66
- namespace: 'MyPlugin',
67
- partials: {
68
- 'MyPlugin.card': '<div>...</div>',
69
- }
70
- }
71
- }
84
+ ```hbs
85
+ <div{{#if className}} class="{{className}}"{{/if}}>
86
+ {{#if title}}<h2>{{title}}</h2>{{/if}}
87
+ {{#if description}}<p>{{description}}</p>{{/if}}
88
+ </div>
72
89
  ```
73
90
 
74
- ### helpers
91
+ This becomes `partials.card`, registered by Hamlet as `Sample.card`:
75
92
 
76
- An object containing Handlebars helpers.
93
+ ```hbs
94
+ {{> Sample.card title="Hello"}}
95
+ ```
96
+
97
+ ### helpers
77
98
 
78
- Every helper name must be prefixed with the namespace followed by an uppercase letter.
99
+ An object of Handlebars helpers keyed by **short camelCase names** (without the namespace prefix). Hamlet registers them as `<namespace><Name>` automatically.
79
100
 
80
101
  Example:
81
102
 
82
103
  ```js
83
104
  export default function hamletPlugin(options = {}) {
84
105
  return {
85
- namespace: 'MyPlugin',
106
+ namespace: 'Sample',
86
107
  helpers: {
87
- MyPluginFormat: value => value.toUpperCase(),
88
- }
108
+ shout: value => value.toUpperCase(),
109
+ },
89
110
  }
90
111
  }
91
112
  ```
92
113
 
93
- Helper names cannot contain dots because Handlebars interprets them as property paths rather than helper names.
114
+ This registers the helper as `SampleShout`, usable in templates as:
115
+
116
+ ```hbs
117
+ {{SampleShout "hello"}}
118
+ ```
119
+
120
+ Helper names cannot contain dots because Handlebars interprets them as property paths.
94
121
 
95
122
  ## Name collisions
96
123
 
97
124
  Hamlet follows a **first registered wins** policy.
98
125
 
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.
126
+ If a plugin attempts to register a partial or helper whose name already exists (whether built-in to Hamlet, from another plugin, or from the user's project), the duplicate is silently skipped and a warning is printed.
100
127
 
101
128
  ## Using Hamlet features inside plugin partials
102
129
 
103
- Plugin partials can freely use Hamlet's built-in helpers and partials.
104
-
105
- For example:
130
+ Plugin partials can freely use Hamlet's built-in helpers and partials:
106
131
 
107
132
  ```hbs
108
133
  {{concat "Hello, " name}}
109
134
 
110
135
  {{#switch value}}
111
- ...
136
+ {{#case "post"}}...{{/case}}
137
+ {{#default}}...{{/default}}
112
138
  {{/switch}}
113
139
 
114
140
  {{> hamlet.snippet}}
115
141
  ```
116
142
 
117
- ## Tests
143
+ ## Scripts
118
144
 
119
- Before publishing:
145
+ | Script | Description |
146
+ | ------ | ----------- |
147
+ | `npm run build:partials` | Generates `lib/partials.js` from files in `src/` |
148
+ | `npm test` | Runs the test suite |
149
+ | `npm run lint` | Lints the project |
150
+ | `npm run lint:fix` | Lints and auto-fixes |
151
+
152
+ `build:partials` also runs automatically via `prepublishOnly` before every `npm publish`.
153
+
154
+ ## Tests
120
155
 
121
156
  ```bash
122
157
  npm install
@@ -125,20 +160,20 @@ npm test
125
160
 
126
161
  The test suite verifies that:
127
162
 
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.
163
+ - The plugin exports a valid factory function.
164
+ - The returned object follows Hamlet's plugin contract.
165
+ - A valid, non-reserved namespace is exported.
166
+ - Partial and helper names are short (no namespace prefix) and follow Hamlet's naming rules.
167
+ - No computed full name collides with Hamlet's reserved partials or helpers.
168
+ - Partials are strings and compile without Handlebars syntax errors.
169
+ - Helpers are functions (not nested objects).
170
+ - No name is on Hamlet's blocked list (`__proto__`, `constructor`, `prototype`).
136
171
 
137
172
  Feel free to extend the tests with plugin-specific behavior or option validation.
138
173
 
139
174
  ## Linting
140
175
 
141
- This template uses the same ESLint configuration as Hamlet itself.
176
+ This template uses the same ESLint configuration as Hamlet itself (`@antfu/eslint-config`).
142
177
 
143
178
  ```bash
144
179
  npm run lint
package/index.js CHANGED
@@ -1,39 +1,31 @@
1
+ import { partials } from './lib/partials.js'
2
+
1
3
  /**
2
4
  * Hamlet plugin template.
3
5
  *
4
6
  * Export a factory function that receives the options from
5
7
  * hamlet.config.js and returns:
6
8
  *
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").
9
+ * - namespace: A unique plugin namespace. Hamlet uses it to
10
+ * prefix all exported partials and helpers automatically,
11
+ * so you only need to define short names here.
12
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.
13
+ * - partials: An object of Handlebars partials. Use short names
14
+ * without the namespace prefix (e.g. "card", not "Sample.card").
15
+ * Hamlet registers them as "<namespace>.<name>" automatically.
16
+ * Usage in templates: {{> Sample.card}}
16
17
  *
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.
18
+ * - helpers: An object of Handlebars helpers. Use short camelCase
19
+ * names without the namespace prefix (e.g. "format", not "SampleFormat").
20
+ * Hamlet registers them as "<namespace><Name>" automatically.
21
+ * Usage in templates: {{SampleFormat value}}
20
22
  *
21
- * @param {object} options - Options from hamlet.config.js.
22
23
  * @return {{namespace: string, partials: object, helpers: object}}
23
24
  */
24
- export default function hamletPlugin(options = {}) {
25
- const { greeting = 'Hello World' } = options
26
-
25
+ export default function hamletPlugin() {
27
26
  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
- },
27
+ namespace: 'Sample',
28
+ partials,
29
+ helpers: {},
38
30
  }
39
31
  }
@@ -0,0 +1,5 @@
1
+ // This file is auto-generated. Do not edit it manually.
2
+ export const partials = {
3
+ "hello": "<p>Hello World!</p>\n",
4
+ "card": "<div{{#if className}} class=\"{{className}}\"{{/if}}>\r\n {{#if title}}<h2>{{title}}</h2>{{/if}}\r\n {{#if description}}<p>{{description}}</p>{{/if}}\r\n</div>\n"
5
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hamlet-plugin-template",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.1.1",
5
5
  "description": "A plugin template for hamlet",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -11,16 +11,22 @@
11
11
  "exports": {
12
12
  ".": "./index.js"
13
13
  },
14
- "main": "index.js",
14
+ "files": [
15
+ "index.js",
16
+ "lib"
17
+ ],
15
18
  "scripts": {
19
+ "build:partials": "node ./scripts/build-partials.js",
20
+ "prepublishOnly": "npm run build:partials",
16
21
  "test": "vitest run",
17
22
  "lint": "eslint .",
18
23
  "lint:fix": "eslint . --fix"
19
24
  },
20
25
  "devDependencies": {
21
26
  "@antfu/eslint-config": "^9.1.0",
22
- "eslint": "^10.6.0",
27
+ "eslint": "^10.7.0",
28
+ "glob": "^13.0.0",
23
29
  "handlebars": "^4.7.9",
24
- "vitest": "^4.1.9"
30
+ "vitest": "^4.1.10"
25
31
  }
26
32
  }
package/.editorconfig DELETED
@@ -1,12 +0,0 @@
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/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import antfu from '@antfu/eslint-config'
2
-
3
- export default antfu()
@@ -1,167 +0,0 @@
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
- })