@otorp/plugin-utils 1.0.0-staging.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/index.js +173 -0
  4. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TDJ.dev, TDN (TISSOT Development Network)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ [![Latest Release](https://gitlab.com/otorp/plugin-utils/-/badges/release.svg)](https://gitlab.com/otorp/plugin-utils/-/releases)
2
+ [![pipeline status](https://gitlab.com/otorp/plugin-utils/badges/main/pipeline.svg)](https://gitlab.com/otorp/plugin-utils/-/commits/main)
3
+
4
+ <!-- omit in toc -->
5
+ # @otorp/plugin-utils
6
+ *Registration helpers for [Otorp](https://gitlab.com/tdj.dev/otorp) plugins*
7
+
8
+ <!-- omit in toc -->
9
+ ## 🎯 Objective
10
+
11
+ Small, init-time utilities extracted from the Otorp core so plugin authors can register modules without duplicating boilerplate or reimplementing the shared symbol registry.
12
+
13
+ This package does not extend prototypes or run Otorp's alias pass — it only wires your plugin into the registry that **Otorp core** consumes during initialization.
14
+
15
+ ---
16
+
17
+ <!-- TOC -->
18
+
19
+ - [📦 Installation](#-installation)
20
+ - [🚀 Quickstart](#-quickstart)
21
+ - [📚 API](#-api)
22
+ - [`setMod(config)`](#setmodconfig)
23
+ - [`newErrorHandler(modName?)`](#newerrorhandlermodname)
24
+ - [`ref(key, value?)`](#refkey-value)
25
+ - [`root`](#root)
26
+ - [🔌 Plugin registration](#-plugin-registration)
27
+ - [With `@otorp/plugin-utils` (recommended)](#with-otorpplugin-utils-recommended)
28
+ - [Manual registration (equivalent)](#manual-registration-equivalent)
29
+ - [❓ FAQ](#-faq)
30
+ - [🤝 Contributing](#-contributing)
31
+
32
+ <!-- /TOC -->
33
+
34
+ ## 📦 Installation
35
+
36
+ > **Load order requirement:** this package — and your plugin script — must be registered **before** Otorp core initializes. Otorp deletes itself after its init pass; any plugin registered after that point will be silently ignored.
37
+
38
+ **npm**
39
+
40
+ ```bash
41
+ npm install @otorp/plugin-utils
42
+ ```
43
+
44
+ ```javascript
45
+ import setMod from '@otorp/plugin-utils';
46
+
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 🚀 Quickstart
52
+
53
+ ```javascript
54
+ import setMod from '@otorp/plugin-utils';
55
+
56
+ setMod({
57
+ source: 'myPlugin',
58
+ endpoints: {
59
+ "HTMLElement": [
60
+ 'myPlugin.customMethod',
61
+ 'myPlugin.meta1.meta2.customMethod'
62
+ ]
63
+ },
64
+ factory(methodName, ...meta) {
65
+ // methodName: last segment of the query string
66
+ // meta: everything between source and methodName
67
+ return function (...args) {
68
+ // `this` is the DOM instance at call time
69
+ };
70
+ }
71
+ });
72
+ ```
73
+
74
+ **Query string anatomy:**
75
+
76
+ ```
77
+ "myPlugin.meta1.meta2.methodName"
78
+ │ │ │ └── methodName (passed as first arg to factory)
79
+ │ └─────┘────────── meta (spread as remaining args to factory)
80
+ └────────────────────────── source (identifies your plugin)
81
+ ```
82
+
83
+ What `setMod` does in one call:
84
+
85
+ 1. Validates the `factory` argument.
86
+ 2. Tracks registration count to warn on duplicate source names.
87
+ 3. Registers `{ endpoints, aliases, key, count }` in the shared modules Map under `source`.
88
+ 4. Creates a unique symbol-like key object with `.throw`, `.warn`, and `.error` helpers attached.
89
+ 5. Assigns the factory to `root` under the returned key so Otorp core can invoke it once per resolved endpoint during init.
90
+
91
+ ---
92
+
93
+ ## 📚 API
94
+
95
+ All exports are init-time helpers. There is no runtime API after Otorp finishes its pass.
96
+
97
+ ### `setMod(config)` (Default Export)
98
+
99
+ Register a plugin module for Otorp's initialization pass.
100
+
101
+ | Property | Type | Required | Description |
102
+ | --- | --- | --- | --- |
103
+ | `source` | `string` | ✓ | Unique module identifier — used as the Map key and as the first segment of endpoint query strings. |
104
+ | `endpoints` | `Record<string, string[]>` | ✓ | Maps a target prototype name to an array of endpoint query strings (e.g. `{ HTMLElement: ['myPlugin.customMethod'] }`). |
105
+ | `factory` | `Function` | ✓ | Called by Otorp once per resolved endpoint. Receives `(methodName, ...meta)` and must return the function to install on the prototype. |
106
+ | `aliases` | `Record<string, string> \| null` | — | Optional alias map. Defaults to `null`. |
107
+
108
+ **Returns:** A unique symbol-like key object (`Object(Symbol(source))`) with `.throw`, `.warn`, and `.error` error handlers attached.
109
+
110
+ ---
111
+
112
+ ### `newErrorHandler(modName?)`
113
+
114
+ Creates a namespaced error handler. The namespace appears in `error.name` so stack traces remain readable.
115
+
116
+ | Callable | Behaviour |
117
+ | :--- | :--- |
118
+ | `handler(msg, opts?)` | Throws an `Error`. Accepts optional `{ type, cause }`. |
119
+ | `handler.log(msg, opts?)` | `console.error` with the same formatting. |
120
+ | `handler.warn(msg, opts?)` | `console.warn` with the same formatting. |
121
+
122
+ ```javascript
123
+ import { newErrorHandler } from '@otorp/plugin-utils';
124
+
125
+ const err = newErrorHandler('myPlugin');
126
+ err('Missing endpoint'); // throws exeption "[myPlugin] Missing endpoint"
127
+ err.log('Deprecated alias used'); // error log "[myPlugin] Deprecated alias used"
128
+ err.warn('Init failed', { cause }); // warning log "[myPlugin - <cause.name>] Init failed"
129
+ ```
130
+
131
+ ---
132
+
133
+ ### `ref(key, value?)`
134
+
135
+ Read or write an entry in the Otorp namespace (`Symbol.for('otorp:' + key)` on `root`). Designed for registries, configuration objects, and shared tooling.
136
+
137
+ * Pass a **truthy** `value` to assign and return it.
138
+ * Omit `value` (or pass a falsy one) to read the current value.
139
+
140
+ > Falsy values cannot be stored through this helper.
141
+
142
+ ---
143
+
144
+ ### `root`
145
+
146
+ Minifier-friendly alias for `window`. Allows bundlers to mangle the identifier across the codebase, reducing bundle size without impacting runtime behavior.
147
+
148
+ ---
149
+
150
+ ## 🔌 Plugin registration
151
+
152
+ ### With `@otorp/plugin-utils` (recommended)
153
+
154
+ ```javascript
155
+ import setMod from '@otorp/plugin-utils';
156
+
157
+ const key = setMod({
158
+ source: 'myPlugin',
159
+ endpoints: { "HTMLElement": ['myPlugin.customMethod'] },
160
+ factory(methodName, ...meta) {
161
+ return function (...args) { /* … */ };
162
+ }
163
+ });
164
+
165
+ key.warn('registered'); // "[myPlugin] registered"
166
+
167
+ ```
168
+
169
+ ### Manual registration (equivalent)
170
+
171
+ If you prefer not to depend on this package, the same contract applies. Core expects a **Map**, tracks registration instances via `count`, and resolves factories from `window` object:
172
+
173
+ ```javascript
174
+ const source = 'myPlugin';
175
+ const pluginKey = Object(Symbol(source));
176
+ const modulesMap = (window[Symbol.for('otorp:modules')] ??= new Map());
177
+
178
+ // Core tracks duplicates, so you must supply the count.
179
+ const count = (modulesMap.get(source)?.count || 0) + 1;
180
+
181
+ modulesMap.set(source, {
182
+ endpoints: { "HTMLElement": ['myPlugin.customMethod'] },
183
+ aliases: null,
184
+ key: pluginKey,
185
+ count
186
+ });
187
+
188
+ // The factory must be exposed on the global object using the exact symbol object.
189
+ window[pluginKey] = function factory(methodName, ...meta) {
190
+ return function (...args) { /* … */ };
191
+ };
192
+ ```
193
+
194
+ `setMod` exists so you do not maintain that boilerplate and the error handler logic in every plugin repo.
195
+
196
+ ---
197
+
198
+ ## ❓ FAQ
199
+
200
+ ### Why a separate package?
201
+
202
+ Otorp originally bundled core, helpers, and plugin utilities together. Splitting `@otorp/plugin-utils` keeps the core lean and gives plugin authors a stable, shared registration layer without copy-pasting registry code.
203
+
204
+ ### Do I need this package to write a plugin?
205
+
206
+ No. Any script that writes the correct shape into `otorp:modules` and `root[key]` before Otorp initializes will work. This package is the supported shortcut.
207
+
208
+ ---
209
+
210
+ ## 🤝 Contributing
211
+
212
+ Contributions, bug reports, and feature requests are welcome.
213
+
214
+ * **Homepage & Wiki:** [gitlab.com/otorp/plugin-utils/-/wikis/home](https://gitlab.com/otorp/plugin-utils/-/wikis/home)
215
+ * **Issue Tracker:** [gitlab.com/otorp/plugin-utils/-/issues](https://gitlab.com/otorp/plugin-utils/-/issues)
216
+ * **Core Otorp Docs:** [gitlab.com/tdj.dev/otorp](https://gitlab.com/tdj.dev/otorp)
package/index.js ADDED
@@ -0,0 +1,173 @@
1
+ // ─── Public exports ───────────────────────────────────────────────────────────
2
+
3
+
4
+ /** Register an Otorp plugin module for the initialization pass.
5
+ *
6
+ * Stores endpoint and aliases metadata in the shared Otorp modules Map,
7
+ * attaches module-scoped error helpers to the returned key, and installs
8
+ * the factory on `root` so Otorp core can resolve it during init.
9
+ *
10
+ * > **Load order:** must be called before Otorp core initializes.
11
+ * > Otorp deletes itself after its init pass — late registrations are ignored.
12
+ *
13
+ * @param {object} config - Plugin registration payload.
14
+ * @param {string} config.source - Unique module identifier. Used as the Map
15
+ * key and as the first segment of endpoint query strings.
16
+ * @param {Record<string, string[]>} config.endpoints - Maps a target prototype
17
+ * name to an array of endpoint query strings.
18
+ * Format: `"source.meta1.meta2.methodName"` — everything between `source`
19
+ * and `methodName` is forwarded to `factory` as metadata.
20
+ * (e.g. `{ "HTMLElement": ['myPlugin.meta.customMethod'] }`)
21
+ * @param {Function} config.factory - Called by Otorp once per resolved
22
+ * endpoint. Receives `(methodName, ...meta)` and must return the function
23
+ * to install on the prototype, where `this` will be the target instance.
24
+ * @param {Record<string, string>|null} [config.aliases] - Optional alias
25
+ * map consumed by Otorp's aliaser during the init pass.
26
+ * @returns {Object} A unique symbol-like key object (`Object(Symbol(source))`)
27
+ * with `.throw`, `.warn`, and `.error` error handlers attached.
28
+ * Otorp resolves the factory via `root[key]` — not through `Symbol.for`.
29
+ *
30
+ * @example
31
+ * import setMod from '@otorp/plugin-utils'
32
+ * const key = setMod({
33
+ * source: 'myPlugin',
34
+ * endpoints: { "HTMLElement": ['myPlugin.meta.customMethod'] },
35
+ * factory(methodName, ...meta) {
36
+ * return function (...args) { /* `this` is the DOM instance *\/ };
37
+ * }
38
+ * });
39
+ *
40
+ * key.warn('registered'); // "[myPlugin] registered"
41
+ */
42
+ export default function ({ source, endpoints, factory, aliases = null }) {
43
+ typeof factory !== 'function' && err("factory must be a function")
44
+
45
+ // Non-global Symbol wrapped in Object() so it can carry properties (.throw,
46
+ // .warn, .error) while remaining usable as a Map/window key.
47
+ // Symbol.for is intentionally avoided — each registration must be isolated.
48
+ const key = Object(Symbol(source)), modErr = newErrorHandler(source);
49
+
50
+ // Attach scoped error helpers directly on the key so plugin authors can use
51
+ // them without importing newErrorHandler separately.
52
+ Object.defineProperties(key, propsDesc(modErr, modErr.log, modErr.warn));
53
+
54
+ // Track registration count so Otorp core can warn on duplicate source names
55
+ // (same source registered twice, or two plugins sharing the same identifier).
56
+ const count = (get(source)?.count || 0) + 1
57
+
58
+ set(source, { endpoints, aliases, key, count });
59
+
60
+ // Install factory under the unique key on root. Otorp reads root[key] during
61
+ // its init pass — it never uses Symbol.for to look up plugin factories.
62
+ return (root[key] = factory), key;
63
+ }
64
+
65
+ /** Creates a namespaced error handler.
66
+ *
67
+ * Returns a callable that throws an `Error`. The namespace appears in
68
+ * `error.name` so stack traces remain readable.
69
+ * `.warn` and `.log` variants emit to the console instead of throwing.
70
+ *
71
+ * @param {string} [modName] - Namespace prefix written to `error.name`.
72
+ * @returns {((msg: string, options?: { type?: string, cause?: any }) => never) & { warn: Function, log: Function }}
73
+ *
74
+ * @example
75
+ * const err = newErrorHandler('myPlugin');
76
+ * err('Missing endpoint'); // throws — name: "[myPlugin]"
77
+ * err.log('Deprecated alias used'); // error log — name: "[myPlugin]"
78
+ * err.warn('Init failed', { cause }); // warns — name: "[myPlugin - <cause.name>]"
79
+ */
80
+ export function newErrorHandler(modName) {
81
+ let [prefix, separator, suffix] = modName ? [`[${modName}`, "-", "]"] : ["", "", ""];
82
+
83
+ /** Builds an Error and delegates to the bound `output` sink. */
84
+ function genErrHandler(msg, { type: errType, cause } = {}) {
85
+ const error = new Error(msg, { cause }), actualType = errType || cause?.name;
86
+ error.name = actualType
87
+ ? (`${prefix} ${separator} ${actualType + suffix}`).trim()
88
+ : prefix + suffix;
89
+ // V8-only: removes genErrHandler from the stack trace.
90
+ // Falls back to a no-op on other engines — full trace is kept.
91
+ (Error.captureStackTrace || (x => x))(error, genErrHandler);
92
+ this.output(error)
93
+ }
94
+
95
+ const myErrHandler = genErrHandler.bind({ output(err) { throw err } })
96
+ myErrHandler.log = genErrHandler.bind({ output(err) { console.error(err) } })
97
+ myErrHandler.warn = genErrHandler.bind({ output(err) { console.warn(err) } })
98
+ return myErrHandler
99
+ }
100
+
101
+ export const
102
+
103
+ /** Minifier-friendly alias for `window`.
104
+ *
105
+ * Allows bundlers to mangle the identifier across the codebase,
106
+ * reducing bundle size without impacting runtime behavior.
107
+ *
108
+ * @type {Window}
109
+ */
110
+ root = window,
111
+
112
+ /** Read or write an entry in the Otorp namespace.
113
+ *
114
+ * Keys are stored under `Symbol.for('otorp:' + key)` on `root`.
115
+ * Designed for registries, configuration objects, and shared tooling —
116
+ * not for arbitrary value storage. Falsy values cannot be written.
117
+ *
118
+ * @param {string} key - Entry name.
119
+ * @param {*} [value] - Truthy value to store. Omit to read.
120
+ * @returns {*} The stored or newly assigned value.
121
+ *
122
+ * @example
123
+ * ref('modules', new Map()); // write
124
+ * ref('modules'); // read → same Map instance
125
+ */
126
+ ref = (key, value) => value
127
+ ? (root[sym(refPrefix + key)] = value)
128
+ : root[sym(refPrefix + key)];
129
+
130
+ // ─── Private ──────────────────────────────────────────────────────────────────
131
+
132
+
133
+ // Builds the descriptor map passed to `Object.defineProperties` on a plugin key.
134
+ // Mutates `propsDescDummy` in place to avoid a per-call object allocation —
135
+ // safe because defineProperties consumes the descriptors synchronously before
136
+ // this function can be called again.
137
+ function propsDesc(_throw, _error, _warn) {
138
+ propsDescDummy.throw.value = _throw
139
+ propsDescDummy.error.value = _error
140
+ propsDescDummy.warn.value = _warn
141
+ return propsDescDummy
142
+ }
143
+
144
+ const
145
+ /** Error handler for this package's own validation and setup failures. */
146
+ err = newErrorHandler('@otorp/plugin-utils'),
147
+
148
+ /** Cached `Symbol.for` to avoid repeated property lookups on each `ref` call. */
149
+ sym = Symbol.for,
150
+
151
+ /** Namespace prefix used by `ref` to scope all Otorp entries on `root`. */
152
+ refPrefix = 'otorp:',
153
+
154
+ /** Bound Map#get and Map#set for the shared modules Map.
155
+ *
156
+ * The Map is retrieved (or created) once at module evaluation time.
157
+ * Methods are bound directly to avoid prototype lookups on every registration.
158
+ */
159
+ [get, set] = (map => [map.get.bind(map), map.set.bind(map)])(
160
+ ref('modules') || ref('modules', new Map)
161
+ ),
162
+
163
+ /** Reusable descriptor map for attaching error helpers to plugin keys.
164
+ *
165
+ * Mutated in place by `propsDesc` before each `Object.defineProperties` call.
166
+ * The defined properties are non-writable and non-configurable on the target
167
+ * to prevent plugin authors from accidentally overwriting the error helpers.
168
+ */
169
+ propsDescDummy = {
170
+ throw: { value: null, writable: false, configurable: false },
171
+ error: { value: null, writable: false, configurable: false },
172
+ warn: { value: null, writable: false, configurable: false },
173
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@otorp/plugin-utils",
3
+ "version": "1.0.0-staging.1",
4
+ "description": "Plugin registration and error-handling utilities for the Otorp ecosystem — minimal, zero side-effects.",
5
+ "exports": {
6
+ ".": "./index.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "index.js"
11
+ ],
12
+ "keywords": [
13
+ "javascript",
14
+ "otorp",
15
+ "plugin",
16
+ "utilities",
17
+ "error-handler",
18
+ "registry"
19
+ ],
20
+ "homepage": "https://gitlab.com/otorp/plugin-utils/-/wikis/home",
21
+ "bugs": {
22
+ "url": "https://gitlab.com/otorp/plugin-utils/-/issues"
23
+ },
24
+ "author": "TISSOT Davy",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://gitlab.com/otorp/plugin-utils.git"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "release": {
34
+ "branches": [
35
+ {
36
+ "name": "main"
37
+ },
38
+ {
39
+ "name": "develop",
40
+ "prerelease": "staging",
41
+ "channel": "staging"
42
+ }
43
+ ],
44
+ "plugins": [
45
+ "@semantic-release/commit-analyzer",
46
+ "@semantic-release/release-notes-generator",
47
+ "@semantic-release/changelog",
48
+ "@semantic-release/npm",
49
+ [
50
+ "@semantic-release/git",
51
+ {
52
+ "assets": [
53
+ "package.json",
54
+ "package-lock.json",
55
+ "CHANGELOG.md"
56
+ ],
57
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
58
+ }
59
+ ],
60
+ "@semantic-release/gitlab"
61
+ ]
62
+ }
63
+ }