@sorb/core 0.1.1 → 0.3.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/package.json +2 -2
- package/src/connectors.test.js +131 -0
- package/src/index.js +288 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorb/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Shared contract for Sorb, the design-token bridge for your running app — typedefs + tier ordering so the contract can't drift. (Core.)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Contract test for the connector registry — proves dispatch is REAL (a
|
|
2
|
+
// registered impl round-trips through get*), not just JSDoc types.
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import {
|
|
6
|
+
connectors,
|
|
7
|
+
registerSource,
|
|
8
|
+
registerCodeSource,
|
|
9
|
+
registerTarget,
|
|
10
|
+
getSource,
|
|
11
|
+
getCodeSource,
|
|
12
|
+
getTarget,
|
|
13
|
+
resolveConnectorIds,
|
|
14
|
+
DEFAULT_SOURCE_ID,
|
|
15
|
+
DEFAULT_CODE_SOURCE_ID,
|
|
16
|
+
DEFAULT_TARGET_ID,
|
|
17
|
+
} from './index.js'
|
|
18
|
+
|
|
19
|
+
test('source connector registers and round-trips via getSource', () => {
|
|
20
|
+
const dummy = {
|
|
21
|
+
id: 'dummy-source',
|
|
22
|
+
async listUnits() {
|
|
23
|
+
return [{ id: 'u1', name: 'Unit 1' }]
|
|
24
|
+
},
|
|
25
|
+
async captureGeometry() {
|
|
26
|
+
return { type: 'FRAME' }
|
|
27
|
+
},
|
|
28
|
+
async readTokens() {
|
|
29
|
+
return { 'color.bg': '#fff' }
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
const returned = registerSource(dummy)
|
|
33
|
+
assert.equal(returned, dummy)
|
|
34
|
+
assert.equal(getSource('dummy-source'), dummy)
|
|
35
|
+
assert.equal(connectors.source.get('dummy-source'), dummy)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('codeSource connector registers and round-trips via getCodeSource', () => {
|
|
39
|
+
const dummy = {
|
|
40
|
+
id: 'dummy-code',
|
|
41
|
+
async resolveAppUrl() {
|
|
42
|
+
return 'http://localhost:5173'
|
|
43
|
+
},
|
|
44
|
+
resolveProjectRoot() {
|
|
45
|
+
return '/tmp/proj'
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
registerCodeSource(dummy)
|
|
49
|
+
assert.equal(getCodeSource('dummy-code'), dummy)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('target adapter registers and round-trips via getTarget', () => {
|
|
53
|
+
const dummy = {
|
|
54
|
+
id: 'dummy-target',
|
|
55
|
+
emitFormat: 'SORB_TOKENSET',
|
|
56
|
+
expectPrefixes: ['bs-'],
|
|
57
|
+
}
|
|
58
|
+
registerTarget(dummy)
|
|
59
|
+
assert.equal(getTarget('dummy-target'), dummy)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('target adapter darkMode field is optional — undefined ⇒ single-mode', () => {
|
|
63
|
+
const dummy = {
|
|
64
|
+
id: 'dummy-target-no-dark',
|
|
65
|
+
emitFormat: 'SORB_TOKENSET',
|
|
66
|
+
expectPrefixes: ['bs-'],
|
|
67
|
+
}
|
|
68
|
+
registerTarget(dummy)
|
|
69
|
+
assert.equal(getTarget('dummy-target-no-dark').darkMode, undefined)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('target adapter round-trips a darkMode convention (attribute strategy)', () => {
|
|
73
|
+
const dummy = {
|
|
74
|
+
id: 'dummy-target-dark',
|
|
75
|
+
emitFormat: 'SORB_TOKENSET',
|
|
76
|
+
expectPrefixes: ['bs-'],
|
|
77
|
+
darkMode: {
|
|
78
|
+
strategy: 'attribute',
|
|
79
|
+
attribute: 'data-bs-theme',
|
|
80
|
+
darkSelector: '[data-bs-theme="dark"]',
|
|
81
|
+
lightSelector: '[data-bs-theme="light"]',
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
registerTarget(dummy)
|
|
85
|
+
const got = getTarget('dummy-target-dark')
|
|
86
|
+
assert.equal(got.darkMode.strategy, 'attribute')
|
|
87
|
+
assert.equal(got.darkMode.attribute, 'data-bs-theme')
|
|
88
|
+
assert.equal(got.darkMode.darkSelector, '[data-bs-theme="dark"]')
|
|
89
|
+
assert.equal(got.darkMode.lightSelector, '[data-bs-theme="light"]')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('get* throws a clear error on unknown id', () => {
|
|
93
|
+
assert.throws(() => getSource('nope'), /Unknown source connector: "nope"/)
|
|
94
|
+
assert.throws(() => getCodeSource('nope'), /Unknown codeSource connector: "nope"/)
|
|
95
|
+
assert.throws(() => getTarget('nope'), /Unknown target adapter: "nope"/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('resolveConnectorIds returns all three defaults for empty config', () => {
|
|
99
|
+
assert.deepEqual(resolveConnectorIds({}), {
|
|
100
|
+
source: DEFAULT_SOURCE_ID,
|
|
101
|
+
codeSource: DEFAULT_CODE_SOURCE_ID,
|
|
102
|
+
target: DEFAULT_TARGET_ID,
|
|
103
|
+
})
|
|
104
|
+
// Same with no argument at all.
|
|
105
|
+
assert.deepEqual(resolveConnectorIds(), {
|
|
106
|
+
source: DEFAULT_SOURCE_ID,
|
|
107
|
+
codeSource: DEFAULT_CODE_SOURCE_ID,
|
|
108
|
+
target: DEFAULT_TARGET_ID,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('resolveConnectorIds overrides only the provided axis', () => {
|
|
113
|
+
assert.deepEqual(resolveConnectorIds({ source: 'x' }), {
|
|
114
|
+
source: 'x',
|
|
115
|
+
codeSource: DEFAULT_CODE_SOURCE_ID,
|
|
116
|
+
target: DEFAULT_TARGET_ID,
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('default ids match the frozen contract', () => {
|
|
121
|
+
assert.equal(DEFAULT_SOURCE_ID, 'storybook-dom')
|
|
122
|
+
assert.equal(DEFAULT_CODE_SOURCE_ID, 'local')
|
|
123
|
+
assert.equal(DEFAULT_TARGET_ID, 'react-bootstrap')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('connectors container is frozen; its axis Maps are mutable', () => {
|
|
127
|
+
assert.ok(Object.isFrozen(connectors))
|
|
128
|
+
assert.ok(connectors.source instanceof Map)
|
|
129
|
+
assert.ok(connectors.codeSource instanceof Map)
|
|
130
|
+
assert.ok(connectors.target instanceof Map)
|
|
131
|
+
})
|
package/src/index.js
CHANGED
|
@@ -98,4 +98,292 @@ export const TIER_RANK = Object.freeze({ component: 0, semantic: 1, primitive: 2
|
|
|
98
98
|
* @property {Object.<string, StoryEntry>} stories storyId → entry.
|
|
99
99
|
*/
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Identifies a component variant by its dot-path prefix.
|
|
103
|
+
* e.g. "button.tertiary" covers all tokens whose id starts with "button.tertiary."
|
|
104
|
+
* @typedef {Object} VariantSpec
|
|
105
|
+
* @property {string} componentId Top-level component key, e.g. "button".
|
|
106
|
+
* @property {string} variantId Full dot-path of the variant, e.g. "button.tertiary".
|
|
107
|
+
* @property {string} [fromVariant] Dot-path of the source variant to clone from (addVariant only).
|
|
108
|
+
* @property {string} [replacedBy] Dot-path that replaces this variant (deprecateVariant only).
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The result of a lifecycle action — what changed.
|
|
113
|
+
* @typedef {Object} VariantChangeset
|
|
114
|
+
* @property {'add'|'deprecate'} action
|
|
115
|
+
* @property {string} variantId The variant that was added or deprecated.
|
|
116
|
+
* @property {string[]} tokenIds All token ids affected (added or deprecated).
|
|
117
|
+
* @property {string} newVersion The component set's new $version after the change.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
// ─── Connector contract (v1: additive) ─────────────────────────────────────────
|
|
121
|
+
//
|
|
122
|
+
// Three pluggable axes — SOURCE (where tokens + geometry come IN), CODE-SOURCE
|
|
123
|
+
// (where the running app / codebase lives), and TARGET (how tokens bind into the
|
|
124
|
+
// app) — plus a runtime registry that dispatches by `id`. Core only defines the
|
|
125
|
+
// contract + registry + default ids; the default implementations are registered
|
|
126
|
+
// by sorb-seed / sorb-juice / sorb-leaf in later phases. See
|
|
127
|
+
// spec/sorb/connectors-architecture.md. `config` everywhere below is the app's
|
|
128
|
+
// `sorb.config.json`-shaped object.
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A design unit to capture — one addressable thing a SourceConnector can turn
|
|
132
|
+
* into geometry (today's Storybook "story entry" is one). Opaque-ish: only `id`
|
|
133
|
+
* is guaranteed; connectors carry whatever extra metadata they need.
|
|
134
|
+
* @typedef {Object} DesignUnit
|
|
135
|
+
* @property {string} id Stable unit id (e.g. a Storybook story id).
|
|
136
|
+
* @property {string} [name] Human-readable label.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* SOURCE axis — where design tokens + geometry come IN. A real source pulls BOTH
|
|
141
|
+
* tokens and geometry from the tool (founder decision 2026-08-28).
|
|
142
|
+
* @typedef {Object} SourceConnector
|
|
143
|
+
* @property {string} id Registry key (default `'storybook-dom'`).
|
|
144
|
+
* @property {(config: Object) => Promise<DesignUnit[]>} listUnits
|
|
145
|
+
* Discover the design units to capture.
|
|
146
|
+
* @property {(unit: DesignUnit, config: Object) => Promise<LayerNode>} captureGeometry
|
|
147
|
+
* Capture one unit as a raw (un-annotated) LayerNode tree.
|
|
148
|
+
* @property {(config: Object) => Promise<TokenSet>} readTokens
|
|
149
|
+
* Read the DTCG token set for this source.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* CODE-SOURCE axis — where the running app / codebase lives.
|
|
154
|
+
* @typedef {Object} CodeSourceConnector
|
|
155
|
+
* @property {string} id Registry key (default `'local'`).
|
|
156
|
+
* @property {(config: Object) => Promise<string|null>} resolveAppUrl
|
|
157
|
+
* Resolve the running app's URL (today = `appUrl` / `localhost:5173`).
|
|
158
|
+
* @property {(config: Object) => string} resolveProjectRoot
|
|
159
|
+
* Resolve the project root dir (today = `process.cwd()`).
|
|
160
|
+
* @property {(config: Object) => Promise<void>} [provision]
|
|
161
|
+
* Optional: clone/build a repo → hosted preview (future code sources).
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* TARGET axis — how tokens bind into the running app (the component-compat seam).
|
|
166
|
+
* @typedef {Object} TargetAdapter
|
|
167
|
+
* @property {string} id Registry key (default `'react-bootstrap'`).
|
|
168
|
+
* @property {string} emitFormat A Style-Dictionary format id (e.g. `SORB_TOKENSET`).
|
|
169
|
+
* @property {string[]} expectPrefixes Vocab-guard namespace(s), e.g. `['bs-']`.
|
|
170
|
+
* @property {(tokens: TokenSet, config: Object) => void} [inject]
|
|
171
|
+
* Optional: bind tokens into non-React hosts (the `sorbInit` seam).
|
|
172
|
+
* @property {DarkModeConvention} [darkMode]
|
|
173
|
+
* Optional: this target's dark-mode convention (real-dark-mode spec D1).
|
|
174
|
+
* Undefined ⇒ single-mode (no dark) — the target has no notion of a dark
|
|
175
|
+
* variant and mode-aware emit/inject should fall back to flat `:root` output.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How a TargetAdapter's host framework expresses light/dark mode. v1 only
|
|
180
|
+
* ships `'attribute'` (Bootstrap 5.3's `[data-bs-theme]`); `'class'`
|
|
181
|
+
* (Tailwind's `.dark`) and `'media'` (OS-only, no manual override) are named
|
|
182
|
+
* here for forward-compat but not yet implemented by any shipped adapter —
|
|
183
|
+
* see real-dark-mode-implementation spec §3 "Deferred to phase 2".
|
|
184
|
+
* @typedef {Object} DarkModeConvention
|
|
185
|
+
* @property {'attribute'|'class'|'media'} strategy
|
|
186
|
+
* How the manual override is expressed. `'attribute'` sets/reads a DOM
|
|
187
|
+
* attribute (e.g. `data-bs-theme`); `'class'` toggles a class on
|
|
188
|
+
* `documentElement`; `'media'` means OS-only, no manual override.
|
|
189
|
+
* @property {string} [attribute]
|
|
190
|
+
* The attribute name for `strategy: 'attribute'` (e.g. `'data-bs-theme'`).
|
|
191
|
+
* @property {string} darkSelector
|
|
192
|
+
* The CSS selector matching the dark-mode override (e.g.
|
|
193
|
+
* `'[data-bs-theme="dark"]'`).
|
|
194
|
+
* @property {string} [lightSelector]
|
|
195
|
+
* The CSS selector matching an explicit light-mode override (e.g.
|
|
196
|
+
* `'[data-bs-theme="light"]'`) — lets a manual "light" choice beat an OS
|
|
197
|
+
* `prefers-color-scheme: dark` setting.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* SEMANTIC-ROLE CONTRACT (framework-targets-productization, T0).
|
|
202
|
+
*
|
|
203
|
+
* The canonical set of role ids a token kit MUST expose for the framework
|
|
204
|
+
* TargetAdapter emit formats (`sorb/mantine-vars`, `sorb/mui-vars`,
|
|
205
|
+
* `sorb/mat-sys-vars`, `sorb/shadcn-theme`, `sorb/primevue-preset`) to emit
|
|
206
|
+
* correctly. Each format maps its framework's own vars (`--mantine-*`, `--mui-*`,
|
|
207
|
+
* `--mat-sys-*`, shadcn vars, PrimeVue preset roots) ONTO these role ids.
|
|
208
|
+
*
|
|
209
|
+
* These are DTCG dot-path ids (→ CSS var `--<kebab>` → preview-payload key
|
|
210
|
+
* `<kebab>`). The Janes Jeans kit (`@metatoy/janes-jeans`) is the reference
|
|
211
|
+
* implementation. A kit using different names supplies `options.roleMap`
|
|
212
|
+
* (role-id → its-own-token-id) to a format rather than forking it.
|
|
213
|
+
*
|
|
214
|
+
* SEMVER: adding a role id here is a MINOR bump; renaming or removing one is a
|
|
215
|
+
* MAJOR bump for @sorb/core AND @sorb/seed — every format consumer depends on
|
|
216
|
+
* this set. Scope = the UNION of the target maps' role columns, not a kit's full
|
|
217
|
+
* token tree; anything beyond this list is kit-private.
|
|
218
|
+
*
|
|
219
|
+
* @typedef {Object} SemanticRoles
|
|
220
|
+
* @property {string[]} color surface/ink/brand/accent/danger/success/border/focus roles.
|
|
221
|
+
* @property {string[]} radius control/card/pill.
|
|
222
|
+
* @property {string[]} shadow raised/overlay.
|
|
223
|
+
* @property {string[]} typography display/heading/body/caption × fontSize/Weight/lineHeight.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The canonical role-id list (T0 reference = the JJ kit's semantic tier).
|
|
228
|
+
* @type {Readonly<{color: string[], radius: string[], shadow: string[], typography: string[]}>}
|
|
229
|
+
*/
|
|
230
|
+
export const DEFAULT_ROLE_IDS = Object.freeze({
|
|
231
|
+
color: Object.freeze([
|
|
232
|
+
'color.surface', 'color.surface-raised', 'color.surface-sunken',
|
|
233
|
+
'color.ink', 'color.ink-muted', 'color.ink-on-brand',
|
|
234
|
+
'color.brand', 'color.brand-hover', 'color.brand-contrast',
|
|
235
|
+
'color.accent', 'color.accent-hover', 'color.accent-contrast',
|
|
236
|
+
'color.danger', 'color.danger-hover', 'color.success', 'color.success-hover',
|
|
237
|
+
'color.focus-ring', 'color.border', 'color.border-subtle', 'color.border-strong',
|
|
238
|
+
]),
|
|
239
|
+
radius: Object.freeze(['radius.control', 'radius.card', 'radius.pill']),
|
|
240
|
+
shadow: Object.freeze(['shadow.raised', 'shadow.overlay']),
|
|
241
|
+
typography: Object.freeze([
|
|
242
|
+
'typography.display.fontSize', 'typography.display.fontWeight', 'typography.display.lineHeight',
|
|
243
|
+
'typography.heading.fontSize', 'typography.heading.fontWeight', 'typography.heading.lineHeight',
|
|
244
|
+
'typography.body.fontSize', 'typography.body.fontWeight', 'typography.body.lineHeight',
|
|
245
|
+
'typography.caption.fontSize', 'typography.caption.fontWeight', 'typography.caption.lineHeight',
|
|
246
|
+
]),
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Flat list of every canonical role id (all tiers), for iteration/validation.
|
|
251
|
+
* @type {readonly string[]}
|
|
252
|
+
*/
|
|
253
|
+
export const ALL_ROLE_IDS = Object.freeze([
|
|
254
|
+
...DEFAULT_ROLE_IDS.color, ...DEFAULT_ROLE_IDS.radius,
|
|
255
|
+
...DEFAULT_ROLE_IDS.shadow, ...DEFAULT_ROLE_IDS.typography,
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve a role id to the kit's actual token id via an optional override map.
|
|
260
|
+
* A format calls `resolveRole('color.brand', options.roleMap)` → the kit's token
|
|
261
|
+
* id (identity when the kit uses canonical ids, i.e. the JJ reference).
|
|
262
|
+
* @param {string} roleId A canonical role id from {@link ALL_ROLE_IDS}.
|
|
263
|
+
* @param {Record<string,string>} [roleMap] role-id → kit-token-id overrides.
|
|
264
|
+
* @returns {string} the kit token id to reference (`var(--<kebab>)`).
|
|
265
|
+
*/
|
|
266
|
+
export function resolveRole(roleId, roleMap) {
|
|
267
|
+
return (roleMap && roleMap[roleId]) || roleId
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Default SOURCE connector id (registered by sorb-seed). @type {string} */
|
|
271
|
+
export const DEFAULT_SOURCE_ID = 'storybook-dom'
|
|
272
|
+
|
|
273
|
+
/** Default CODE-SOURCE connector id (registered by sorb-juice). @type {string} */
|
|
274
|
+
export const DEFAULT_CODE_SOURCE_ID = 'local'
|
|
275
|
+
|
|
276
|
+
/** Default TARGET adapter id (registered by sorb-leaf). @type {string} */
|
|
277
|
+
export const DEFAULT_TARGET_ID = 'react-bootstrap'
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The runtime connector registry — id → impl per axis. The Maps are mutable by
|
|
281
|
+
* design so consumer packages register their defaults into them; the container
|
|
282
|
+
* itself is frozen so the axis set can't drift.
|
|
283
|
+
* @typedef {Object} ConnectorRegistry
|
|
284
|
+
* @property {Map<string, SourceConnector>} source
|
|
285
|
+
* @property {Map<string, CodeSourceConnector>} codeSource
|
|
286
|
+
* @property {Map<string, TargetAdapter>} target
|
|
287
|
+
* @type {ConnectorRegistry}
|
|
288
|
+
*/
|
|
289
|
+
export const connectors = Object.freeze({
|
|
290
|
+
source: new Map(),
|
|
291
|
+
codeSource: new Map(),
|
|
292
|
+
target: new Map(),
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Register a SOURCE connector by its `id`.
|
|
297
|
+
* @param {SourceConnector} conn
|
|
298
|
+
* @returns {SourceConnector} the registered connector.
|
|
299
|
+
*/
|
|
300
|
+
export function registerSource(conn) {
|
|
301
|
+
connectors.source.set(conn.id, conn)
|
|
302
|
+
return conn
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Register a CODE-SOURCE connector by its `id`.
|
|
307
|
+
* @param {CodeSourceConnector} conn
|
|
308
|
+
* @returns {CodeSourceConnector} the registered connector.
|
|
309
|
+
*/
|
|
310
|
+
export function registerCodeSource(conn) {
|
|
311
|
+
connectors.codeSource.set(conn.id, conn)
|
|
312
|
+
return conn
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Register a TARGET adapter by its `id`. Minimal shape validation (T0b) — with
|
|
317
|
+
* seven+ adapters registering into one Map, a typo'd `id` or missing
|
|
318
|
+
* `emitFormat` would fail silently at query time; catch it at register time.
|
|
319
|
+
* Throws on a malformed adapter; `console.warn`s (does not throw) on a
|
|
320
|
+
* duplicate-id overwrite so a legitimate re-register in tests/HMR still works.
|
|
321
|
+
* @param {TargetAdapter} adapter
|
|
322
|
+
* @returns {TargetAdapter} the registered adapter.
|
|
323
|
+
*/
|
|
324
|
+
export function registerTarget(adapter) {
|
|
325
|
+
if (!adapter || typeof adapter.id !== 'string' || !adapter.id) {
|
|
326
|
+
throw new Error('registerTarget: adapter.id must be a non-empty string')
|
|
327
|
+
}
|
|
328
|
+
if (typeof adapter.emitFormat !== 'string' || !adapter.emitFormat) {
|
|
329
|
+
throw new Error(`registerTarget(${JSON.stringify(adapter.id)}): emitFormat must be a non-empty string`)
|
|
330
|
+
}
|
|
331
|
+
if (!Array.isArray(adapter.expectPrefixes)) {
|
|
332
|
+
throw new Error(`registerTarget(${JSON.stringify(adapter.id)}): expectPrefixes must be an array`)
|
|
333
|
+
}
|
|
334
|
+
if (connectors.target.has(adapter.id)) {
|
|
335
|
+
// eslint-disable-next-line no-console
|
|
336
|
+
console.warn(`registerTarget: overwriting existing target adapter ${JSON.stringify(adapter.id)}`)
|
|
337
|
+
}
|
|
338
|
+
connectors.target.set(adapter.id, adapter)
|
|
339
|
+
return adapter
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Look up a registered SOURCE connector; throws on unknown id.
|
|
344
|
+
* @param {string} id
|
|
345
|
+
* @returns {SourceConnector}
|
|
346
|
+
*/
|
|
347
|
+
export function getSource(id) {
|
|
348
|
+
const conn = connectors.source.get(id)
|
|
349
|
+
if (!conn) throw new Error(`Unknown source connector: ${JSON.stringify(id)}`)
|
|
350
|
+
return conn
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Look up a registered CODE-SOURCE connector; throws on unknown id.
|
|
355
|
+
* @param {string} id
|
|
356
|
+
* @returns {CodeSourceConnector}
|
|
357
|
+
*/
|
|
358
|
+
export function getCodeSource(id) {
|
|
359
|
+
const conn = connectors.codeSource.get(id)
|
|
360
|
+
if (!conn) throw new Error(`Unknown codeSource connector: ${JSON.stringify(id)}`)
|
|
361
|
+
return conn
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Look up a registered TARGET adapter; throws on unknown id.
|
|
366
|
+
* @param {string} id
|
|
367
|
+
* @returns {TargetAdapter}
|
|
368
|
+
*/
|
|
369
|
+
export function getTarget(id) {
|
|
370
|
+
const adapter = connectors.target.get(id)
|
|
371
|
+
if (!adapter) throw new Error(`Unknown target adapter: ${JSON.stringify(id)}`)
|
|
372
|
+
return adapter
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resolve the three connector ids from a config, falling back to the defaults
|
|
377
|
+
* when a key is absent (back-compat = today's behavior).
|
|
378
|
+
* @param {{ source?: string, codeSource?: string, target?: string }} [config]
|
|
379
|
+
* @returns {{ source: string, codeSource: string, target: string }}
|
|
380
|
+
*/
|
|
381
|
+
export function resolveConnectorIds(config = {}) {
|
|
382
|
+
return {
|
|
383
|
+
source: config.source || DEFAULT_SOURCE_ID,
|
|
384
|
+
codeSource: config.codeSource || DEFAULT_CODE_SOURCE_ID,
|
|
385
|
+
target: config.target || DEFAULT_TARGET_ID,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
101
389
|
export {}
|