assign-gingerly 0.0.21 → 0.0.23

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.
@@ -1,4 +1,4 @@
1
- import assignGingerly, { EnhancementRegistry, getInstanceMap } from './assignGingerly.js';
1
+ import assignGingerly, { EnhancementRegistry, ItemscopeRegistry, getInstanceMap } from './assignGingerly.js';
2
2
  import { parseWithAttrs } from './parseWithAttrs.js';
3
3
  /**
4
4
  * Normalizes lifecycleKeys to always return an object with dispose and resolved keys
@@ -34,6 +34,25 @@ if (typeof CustomElementRegistry !== 'undefined') {
34
34
  enumerable: false,
35
35
  configurable: true,
36
36
  });
37
+ /**
38
+ * Adds itemscopeRegistry to CustomElementRegistry prototype as a lazy getter
39
+ */
40
+ Object.defineProperty(CustomElementRegistry.prototype, 'itemscopeRegistry', {
41
+ get: function () {
42
+ // Create a new ItemscopeRegistry instance on first access and cache it
43
+ const registry = new ItemscopeRegistry();
44
+ // Replace the getter with the actual value
45
+ Object.defineProperty(this, 'itemscopeRegistry', {
46
+ value: registry,
47
+ writable: true,
48
+ enumerable: false,
49
+ configurable: true,
50
+ });
51
+ return registry;
52
+ },
53
+ enumerable: false,
54
+ configurable: true,
55
+ });
37
56
  }
38
57
  /**
39
58
  * Enhancement container class for Element.prototype.enh
@@ -1,4 +1,4 @@
1
- import assignGingerly, { EnhancementRegistry, IAssignGingerlyOptions, getInstanceMap, INSTANCE_MAP_GUID } from './assignGingerly.js';
1
+ import assignGingerly, { EnhancementRegistry, ItemscopeRegistry, IAssignGingerlyOptions, getInstanceMap, INSTANCE_MAP_GUID } from './assignGingerly.js';
2
2
  import { parseWithAttrs } from './parseWithAttrs.js';
3
3
 
4
4
  /**
@@ -17,11 +17,12 @@ function normalizeLifecycleKeys(lifecycleKeys: true | { dispose?: string | symbo
17
17
  }
18
18
 
19
19
  /**
20
- * Extends the CustomElementRegistry interface to include enhancementRegistry
20
+ * Extends the CustomElementRegistry interface to include enhancementRegistry and itemscopeRegistry
21
21
  */
22
22
  declare global {
23
23
  interface CustomElementRegistry {
24
24
  enhancementRegistry: typeof EnhancementRegistry | EnhancementRegistry;
25
+ itemscopeRegistry: ItemscopeRegistry;
25
26
  }
26
27
 
27
28
  interface Element {
@@ -97,6 +98,26 @@ if (typeof CustomElementRegistry !== 'undefined') {
97
98
  enumerable: false,
98
99
  configurable: true,
99
100
  });
101
+
102
+ /**
103
+ * Adds itemscopeRegistry to CustomElementRegistry prototype as a lazy getter
104
+ */
105
+ Object.defineProperty(CustomElementRegistry.prototype, 'itemscopeRegistry', {
106
+ get: function () {
107
+ // Create a new ItemscopeRegistry instance on first access and cache it
108
+ const registry = new ItemscopeRegistry();
109
+ // Replace the getter with the actual value
110
+ Object.defineProperty(this, 'itemscopeRegistry', {
111
+ value: registry,
112
+ writable: true,
113
+ enumerable: false,
114
+ configurable: true,
115
+ });
116
+ return registry;
117
+ },
118
+ enumerable: false,
119
+ configurable: true,
120
+ });
100
121
  }
101
122
 
102
123
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -58,12 +58,14 @@
58
58
  "scripts": {
59
59
  "serve": "node ./node_modules/spa-ssi/serve.js",
60
60
  "test": "playwright test",
61
- "update": "ncu -u && npm install"
61
+ "update": "ncu -u && npm install",
62
+ "safari": "npx playwright wk http://localhost:8000",
63
+ "chrome": "npx playwright cr http://localhost:8000"
62
64
  },
63
65
  "devDependencies": {
64
- "@playwright/test": "^1.58.2",
66
+ "@playwright/test": "1.59.0-alpha-2026-02-28",
65
67
  "spa-ssi": "0.0.27",
66
- "@types/node": "^25.3.0",
68
+ "@types/node": "^25.3.3",
67
69
  "typescript": "^5.9.3"
68
70
  }
69
71
  }
package/parseWithAttrs.js CHANGED
@@ -23,35 +23,38 @@ function resolveParser(parserSpec) {
23
23
  if (typeof parserSpec === 'function') {
24
24
  return parserSpec;
25
25
  }
26
- // String reference - resolve it
27
- if (typeof parserSpec === 'string') {
28
- // Check if it's a custom element reference (contains dot)
29
- if (parserSpec.includes('.')) {
30
- const dotIndex = parserSpec.indexOf('.');
31
- const elementName = parserSpec.substring(0, dotIndex);
32
- const methodName = parserSpec.substring(dotIndex + 1);
33
- // Try custom element lookup
34
- if (typeof customElements !== 'undefined') {
35
- try {
36
- const ctr = customElements.get(elementName);
37
- if (ctr && typeof ctr[methodName] === 'function') {
38
- return ctr[methodName];
39
- }
40
- }
41
- catch (e) {
42
- // customElements.get might throw, fall through to registry
43
- }
26
+ // Tuple [CustomElementName, StaticMethodName] - resolve custom element static method
27
+ if (Array.isArray(parserSpec)) {
28
+ const [elementName, methodName] = parserSpec;
29
+ if (typeof customElements === 'undefined') {
30
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: customElements is not available`);
31
+ }
32
+ try {
33
+ const ctr = customElements.get(elementName);
34
+ if (!ctr) {
35
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`);
44
36
  }
45
- // Fall through to global registry (allows dot notation in registry too)
37
+ if (typeof ctr[methodName] !== 'function') {
38
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`);
39
+ }
40
+ return ctr[methodName];
41
+ }
42
+ catch (e) {
43
+ if (e instanceof Error && e.message.startsWith('Cannot resolve parser')) {
44
+ throw e;
45
+ }
46
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: ${e instanceof Error ? e.message : String(e)}`);
46
47
  }
47
- // Try global registry
48
+ }
49
+ // String reference - resolve from global registry
50
+ if (typeof parserSpec === 'string') {
48
51
  const parser = globalParserRegistry.get(parserSpec);
49
52
  if (parser) {
50
53
  return parser;
51
54
  }
52
- // Not found anywhere
53
- throw new Error(`Parser "${parserSpec}" not found. ` +
54
- `Check that it's registered in globalParserRegistry or exists as a static method on the custom element.`);
55
+ // Not found in registry
56
+ throw new Error(`Parser "${parserSpec}" not found in globalParserRegistry. ` +
57
+ `If you want to reference a custom element static method, use tuple syntax: ["element-name", "methodName"]`);
55
58
  }
56
59
  return undefined;
57
60
  }
@@ -71,6 +74,9 @@ function getCacheKey(config) {
71
74
  else if (typeof config.parser === 'string') {
72
75
  parserStr = `named:${config.parser}`;
73
76
  }
77
+ else if (Array.isArray(config.parser)) {
78
+ parserStr = `tuple:${config.parser[0]}.${config.parser[1]}`;
79
+ }
74
80
  else {
75
81
  parserStr = 'custom';
76
82
  }
package/parseWithAttrs.ts CHANGED
@@ -17,7 +17,9 @@ const parseCache = new Map<string, Map<string, any>>();
17
17
  * @returns The resolved parser function
18
18
  * @throws Error if parser cannot be resolved
19
19
  */
20
- function resolveParser(parserSpec: ((v: string | null) => any) | string | undefined): ((v: string | null) => any) | undefined {
20
+ function resolveParser(
21
+ parserSpec: ((v: string | null) => any) | string | [string, string] | undefined
22
+ ): ((v: string | null) => any) | undefined {
21
23
  // Undefined - no parser specified
22
24
  if (parserSpec === undefined) {
23
25
  return undefined;
@@ -28,39 +30,52 @@ function resolveParser(parserSpec: ((v: string | null) => any) | string | undefi
28
30
  return parserSpec;
29
31
  }
30
32
 
31
- // String reference - resolve it
32
- if (typeof parserSpec === 'string') {
33
- // Check if it's a custom element reference (contains dot)
34
- if (parserSpec.includes('.')) {
35
- const dotIndex = parserSpec.indexOf('.');
36
- const elementName = parserSpec.substring(0, dotIndex);
37
- const methodName = parserSpec.substring(dotIndex + 1);
33
+ // Tuple [CustomElementName, StaticMethodName] - resolve custom element static method
34
+ if (Array.isArray(parserSpec)) {
35
+ const [elementName, methodName] = parserSpec;
36
+
37
+ if (typeof customElements === 'undefined') {
38
+ throw new Error(
39
+ `Cannot resolve parser [${elementName}, ${methodName}]: customElements is not available`
40
+ );
41
+ }
42
+
43
+ try {
44
+ const ctr = customElements.get(elementName);
45
+ if (!ctr) {
46
+ throw new Error(
47
+ `Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`
48
+ );
49
+ }
38
50
 
39
- // Try custom element lookup
40
- if (typeof customElements !== 'undefined') {
41
- try {
42
- const ctr = customElements.get(elementName);
43
- if (ctr && typeof (ctr as any)[methodName] === 'function') {
44
- return (ctr as any)[methodName];
45
- }
46
- } catch (e) {
47
- // customElements.get might throw, fall through to registry
48
- }
51
+ if (typeof (ctr as any)[methodName] !== 'function') {
52
+ throw new Error(
53
+ `Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`
54
+ );
49
55
  }
50
56
 
51
- // Fall through to global registry (allows dot notation in registry too)
57
+ return (ctr as any)[methodName];
58
+ } catch (e) {
59
+ if (e instanceof Error && e.message.startsWith('Cannot resolve parser')) {
60
+ throw e;
61
+ }
62
+ throw new Error(
63
+ `Cannot resolve parser [${elementName}, ${methodName}]: ${e instanceof Error ? e.message : String(e)}`
64
+ );
52
65
  }
53
-
54
- // Try global registry
66
+ }
67
+
68
+ // String reference - resolve from global registry
69
+ if (typeof parserSpec === 'string') {
55
70
  const parser = globalParserRegistry.get(parserSpec);
56
71
  if (parser) {
57
72
  return parser;
58
73
  }
59
74
 
60
- // Not found anywhere
75
+ // Not found in registry
61
76
  throw new Error(
62
- `Parser "${parserSpec}" not found. ` +
63
- `Check that it's registered in globalParserRegistry or exists as a static method on the custom element.`
77
+ `Parser "${parserSpec}" not found in globalParserRegistry. ` +
78
+ `If you want to reference a custom element static method, use tuple syntax: ["element-name", "methodName"]`
64
79
  );
65
80
  }
66
81
 
@@ -82,6 +97,8 @@ function getCacheKey(config: AttrConfig<any>): string {
82
97
  parserStr = 'builtin';
83
98
  } else if (typeof config.parser === 'string') {
84
99
  parserStr = `named:${config.parser}`;
100
+ } else if (Array.isArray(config.parser)) {
101
+ parserStr = `tuple:${config.parser[0]}.${config.parser[1]}`;
85
102
  } else {
86
103
  parserStr = 'custom';
87
104
  }
@@ -0,0 +1,42 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * Playwright configuration for running tests in actual browser JavaScript engines.
5
+ * This configuration opens HTML test pages in real browsers instead of running
6
+ * tests in Node.js.
7
+ */
8
+ export default defineConfig({
9
+ testDir: './tests-browser',
10
+ fullyParallel: true,
11
+ forbidOnly: !!process.env.CI,
12
+ retries: process.env.CI ? 2 : 0,
13
+ workers: process.env.CI ? 1 : undefined,
14
+ reporter: 'html',
15
+
16
+ use: {
17
+ baseURL: 'http://localhost:8000',
18
+ trace: 'on-first-retry',
19
+ },
20
+
21
+ projects: [
22
+ {
23
+ name: 'chromium',
24
+ use: { ...devices['Desktop Chrome'] },
25
+ },
26
+ {
27
+ name: 'firefox',
28
+ use: { ...devices['Desktop Firefox'] },
29
+ },
30
+ {
31
+ name: 'webkit',
32
+ use: { ...devices['Desktop Safari'] },
33
+ },
34
+ ],
35
+
36
+ // Start local web server before running tests
37
+ webServer: {
38
+ command: 'npm run serve',
39
+ port: 8000,
40
+ reuseExistingServer: !process.env.CI,
41
+ },
42
+ });
@@ -20,11 +20,11 @@ export default defineConfig({
20
20
  /* Opt out of parallel tests on CI. */
21
21
  workers: process.env.CI ? 1 : undefined,
22
22
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23
- reporter: 'html',
23
+ reporter: [ ['html', { open: 'never' }] ],
24
24
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25
25
  use: {
26
26
  /* Base URL to use in actions like `await page.goto('/')`. */
27
- baseURL: 'http://localhost:5173',
27
+ baseURL: 'http://localhost:8000',
28
28
  },
29
29
 
30
30
  /* Configure projects for major browsers */
@@ -34,21 +34,21 @@ export default defineConfig({
34
34
  use: { ...devices['Desktop Chrome'] },
35
35
  },
36
36
 
37
- {
38
- name: 'firefox',
39
- use: { ...devices['Desktop Firefox'] },
40
- },
37
+ // {
38
+ // name: 'firefox',
39
+ // use: { ...devices['Desktop Firefox'] },
40
+ // },
41
41
 
42
- {
43
- name: 'webkit',
44
- use: { ...devices['Desktop Safari'] },
45
- },
42
+ // {
43
+ // name: 'webkit',
44
+ // use: { ...devices['Desktop Safari'] },
45
+ // },
46
46
  ],
47
47
 
48
48
  /* Run your local dev server before starting the tests */
49
- // webServer: {
50
- // command: 'npm run dev',
51
- // url: 'http://localhost:5173',
52
- // reuseExistingServer: !process.env.CI,
53
- // },
49
+ webServer: {
50
+ command: 'npm run serve',
51
+ port: 8000,
52
+ reuseExistingServer: !process.env.CI,
53
+ },
54
54
  });
@@ -66,6 +66,9 @@ export type Constructor = new (...args: any[]) => any;
66
66
 
67
67
  export type pathString = `?.${string}`;
68
68
 
69
+ export type CustomElementName = string;
70
+ export type CustomElementConstructorStaticMethodName = string;
71
+
69
72
  export interface AttrConfig<T = any> {
70
73
  /**
71
74
  * Type of the property value (JSON-serializable string format)
@@ -92,12 +95,14 @@ export interface AttrConfig<T = any> {
92
95
  /**
93
96
  * Parser to transform attribute string value
94
97
  * - Function: Inline parser function (not JSON serializable)
95
- * - String: Named parser reference (JSON serializable)
96
- * - Simple name: Looks up in global parser registry (e.g., 'timestamp', 'csv')
97
- * - Dot notation: Looks up static method on custom element (e.g., 'my-widget.parseSpecial')
98
- * Falls back to global registry if custom element not found
98
+ * - String: Named parser reference (JSON serializable) - looks up in global parser registry (e.g., 'timestamp', 'csv')
99
+ * - Tuple: [CustomElementName, StaticMethodName] - looks up static method on custom element constructor (e.g., ['my-widget', 'parseSpecial'])
99
100
  */
100
- parser?: ((attrValue: string | null) => any) | string;
101
+ parser?:
102
+ | ((attrValue: string | null) => any)
103
+ | string
104
+ | [CustomElementName, CustomElementConstructorStaticMethodName]
105
+ ;
101
106
 
102
107
  /**
103
108
  * Default value to use when attribute is missing
@@ -161,10 +166,20 @@ export interface IAssignGingerlyOptions {
161
166
  bypassChecks?: boolean;
162
167
  }
163
168
 
169
+ /**
170
+ * Event dispatched when enhancement configs are registered
171
+ */
172
+ export declare class EnhancementRegisteredEvent extends Event {
173
+ static eventName: string;
174
+ config: EnhancementConfig | EnhancementConfig[];
175
+ constructor(config: EnhancementConfig | EnhancementConfig[]);
176
+ }
177
+
164
178
  /**
165
179
  * Base registry class for managing enhancement configurations
180
+ * Extends EventTarget to dispatch events when configs are registered
166
181
  */
167
- export declare class EnhancementRegistry {
182
+ export declare class EnhancementRegistry extends EventTarget {
168
183
  private items;
169
184
  push(items: EnhancementConfig | EnhancementConfig[]): void;
170
185
  getItems(): EnhancementConfig[];
@@ -172,6 +187,43 @@ export declare class EnhancementRegistry {
172
187
  findByEnhKey(enhKey: string | symbol): EnhancementConfig | undefined;
173
188
  }
174
189
 
190
+ /**
191
+ * Constructor signature for ItemScope Manager classes
192
+ */
193
+ export type ItemscopeManager<T = any> = {
194
+ new (element: HTMLElement, initVals?: Partial<T>): T;
195
+ }
196
+
197
+ /**
198
+ * Configuration for ItemScope Manager registration
199
+ */
200
+ export interface ItemscopeManagerConfig<T = any> {
201
+ /**
202
+ * Manager class constructor
203
+ */
204
+ manager: ItemscopeManager<T>;
205
+
206
+ /**
207
+ * Optional lifecycle method keys
208
+ * - dispose: Method name to call when manager is disposed
209
+ * - resolved: Property/event name indicating manager is ready
210
+ */
211
+ lifecycleKeys?: {
212
+ dispose?: string | symbol;
213
+ resolved?: string | symbol;
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Registry for ItemScope Manager configurations
219
+ * Extends EventTarget to support lazy registration via events
220
+ */
221
+ export declare class ItemscopeRegistry extends EventTarget {
222
+ define(name: string, config: ItemscopeManagerConfig): void;
223
+ get(name: string): ItemscopeManagerConfig | undefined;
224
+ whenDefined(name: string): Promise<void>;
225
+ }
226
+
175
227
  /**
176
228
  * Main assignGingerly function
177
229
  */
package/types/global.d.ts CHANGED
@@ -23,3 +23,7 @@ interface WeakMap<K extends object, V> {
23
23
  */
24
24
  getOrInsertComputed(key: K, insert: () => V): V;
25
25
  }
26
+
27
+ interface HTMLTemplateElement {
28
+ remoteContent?: Node;
29
+ }