assign-gingerly 0.0.72 → 0.0.74
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 +29 -92
- package/assignFeatures.js +145 -254
- package/assignFeatures.ts +713 -837
- package/assignFrom.ts +1 -1
- package/assignFromAsync.ts +1 -1
- package/assignGingerly.js +1 -1
- package/assignGingerly.ts +3 -2
- package/assignPermissions/isAllowedImportPath.js +41 -0
- package/assignPermissions/isAllowedImportPath.ts +38 -0
- package/assignPermissions/restrictedProps.js +43 -0
- package/assignPermissions/restrictedProps.ts +53 -0
- package/assignTentatively.js +1 -1
- package/assignTentatively.ts +2 -2
- package/defineWithFeatures.js +12 -72
- package/defineWithFeatures.ts +92 -167
- package/eachTime.js +1 -1
- package/eachTime.ts +3 -2
- package/enhanceAll.js +10 -6
- package/enhanceAll.ts +11 -10
- package/handlers/addEventListener.ts +1 -1
- package/handlers/lazyLoad.ts +1 -1
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.ts +1 -1
- package/handlers/rangeSelector.ts +1 -1
- package/inferencer/types/assign-gingerly/types.d.ts +5 -2
- package/inferencer/types/font-face-feature/types.d.ts +9 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +12 -0
- package/package.json +13 -4
- package/processHandlerCommands.js +12 -34
- package/processHandlerCommands.ts +15 -41
- package/resolveAndAssignFeatures.js +9 -40
- package/resolveAndAssignFeatures.ts +43 -79
- package/types/assign-gingerly/types.d.ts +13 -25
- package/isAllowedImportPath.js +0 -104
- package/isAllowedImportPath.ts +0 -118
package/assignFrom.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { getValues, getValue } from './getValues.js';
|
|
|
13
13
|
import assignGingerly from './assignGingerly.js';
|
|
14
14
|
import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
|
|
15
15
|
import { processInferredAssignments } from './inferredAssignments.js';
|
|
16
|
-
import type { AssignPermissions } from './
|
|
16
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
17
17
|
import type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
|
|
18
18
|
|
|
19
19
|
// Re-export types for consumers
|
package/assignFromAsync.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { resolveValues } from './resolveValues.js';
|
|
23
23
|
import {IAssignGingerlyOptions} from './types/assign-gingerly/types.js';
|
|
24
24
|
import assignGingerly from './assignGingerly.js';
|
|
25
|
-
import type { AssignPermissions } from './
|
|
25
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
26
26
|
import type { AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
|
|
27
27
|
import {
|
|
28
28
|
expandSubstitutions, categorizeKeys, handleSpreads, isHandlerCommand
|
package/assignGingerly.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './
|
|
1
|
+
import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
2
2
|
import { normalizeAliasOptions } from './getValues.js';
|
|
3
3
|
/**
|
|
4
4
|
* GUID for global instance map storage to ensure uniqueness across package versions
|
package/assignGingerly.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
4
4
|
import type { AssignFromOptions, FeatureConfigsMap, IAssignGingerlyOptions } from "./types/assign-gingerly/types";
|
|
5
|
-
import type { AssignPermissions
|
|
6
|
-
import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './
|
|
5
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
6
|
+
import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
7
|
+
import type { RestrictedPropSettingsMap } from './assignPermissions/restrictedProps.js';
|
|
7
8
|
import { normalizeAliasOptions } from './getValues.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether an import specifier resolves to the current origin or is
|
|
3
|
+
* covered by an import-map entry.
|
|
4
|
+
*/
|
|
5
|
+
export function isAllowedImportPath(value) {
|
|
6
|
+
if (typeof document === 'undefined' || typeof location === 'undefined')
|
|
7
|
+
return false;
|
|
8
|
+
if (!value || !(value.startsWith('./') || value.startsWith('../') || value.startsWith('/'))) {
|
|
9
|
+
return isImportMapSpecifier(value);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return new URL(value, document.baseURI).origin === location.origin;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isImportMapSpecifier(value) {
|
|
19
|
+
const importMaps = document.querySelectorAll('script[type="importmap"]');
|
|
20
|
+
for (const importMap of importMaps) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(importMap.textContent ?? '');
|
|
23
|
+
if (!isImportMap(parsed))
|
|
24
|
+
continue;
|
|
25
|
+
for (const key of Object.keys(parsed.imports)) {
|
|
26
|
+
if (key.endsWith('/') ? value.startsWith(key) : value === key)
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Ignore malformed import maps and continue searching.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function isImportMap(value) {
|
|
37
|
+
if (!value || typeof value !== 'object' || !('imports' in value))
|
|
38
|
+
return false;
|
|
39
|
+
const { imports } = value;
|
|
40
|
+
return !!imports && typeof imports === 'object' && !Array.isArray(imports);
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether an import specifier resolves to the current origin or is
|
|
3
|
+
* covered by an import-map entry.
|
|
4
|
+
*/
|
|
5
|
+
export function isAllowedImportPath(value: string): boolean {
|
|
6
|
+
if (typeof document === 'undefined' || typeof location === 'undefined') return false;
|
|
7
|
+
if (!value || !(value.startsWith('./') || value.startsWith('../') || value.startsWith('/'))) {
|
|
8
|
+
return isImportMapSpecifier(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
return new URL(value, document.baseURI).origin === location.origin;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isImportMapSpecifier(value: string): boolean {
|
|
19
|
+
const importMaps = document.querySelectorAll('script[type="importmap"]');
|
|
20
|
+
for (const importMap of importMaps) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(importMap.textContent ?? '');
|
|
23
|
+
if (!isImportMap(parsed)) continue;
|
|
24
|
+
for (const key of Object.keys(parsed.imports)) {
|
|
25
|
+
if (key.endsWith('/') ? value.startsWith(key) : value === key) return true;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore malformed import maps and continue searching.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isImportMap(value: unknown): value is { imports: Record<string, unknown> } {
|
|
35
|
+
if (!value || typeof value !== 'object' || !('imports' in value)) return false;
|
|
36
|
+
const { imports } = value;
|
|
37
|
+
return !!imports && typeof imports === 'object' && !Array.isArray(imports);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const warnedOnce = new Set();
|
|
2
|
+
function warnRestricted(key) {
|
|
3
|
+
if (!warnedOnce.has(key)) {
|
|
4
|
+
warnedOnce.add(key);
|
|
5
|
+
console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function buildRestrictedPropSet(permissions) {
|
|
9
|
+
const settings = permissions?.restrictedPropSettings;
|
|
10
|
+
if (!settings || settings.length === 0)
|
|
11
|
+
return undefined;
|
|
12
|
+
const restrictedPropSet = new Map();
|
|
13
|
+
for (const setting of settings) {
|
|
14
|
+
const prop = typeof setting === 'string' ? setting : setting.prop;
|
|
15
|
+
if (restrictedPropSet.has(prop)) {
|
|
16
|
+
throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
|
|
17
|
+
}
|
|
18
|
+
restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
|
|
19
|
+
}
|
|
20
|
+
return restrictedPropSet;
|
|
21
|
+
}
|
|
22
|
+
export function checkRestrictedProp(restrictedPropSet, key) {
|
|
23
|
+
if (!restrictedPropSet || !restrictedPropSet.has(key))
|
|
24
|
+
return false;
|
|
25
|
+
warnRestricted(key);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
export function redirectRestrictedProp(restrictedPropSet, target, key, value) {
|
|
29
|
+
if (!restrictedPropSet || !restrictedPropSet.has(key))
|
|
30
|
+
return false;
|
|
31
|
+
const setting = restrictedPropSet.get(key);
|
|
32
|
+
if (!setting?.useMethod) {
|
|
33
|
+
warnRestricted(key);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const method = target?.[setting.useMethod];
|
|
37
|
+
if (typeof method !== 'function') {
|
|
38
|
+
warnRestricted(key);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
method.call(target, value);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
|
|
2
|
+
|
|
3
|
+
export type RestrictedPropSettingsMap = Map<string, RestrictedPropSetting | undefined>;
|
|
4
|
+
|
|
5
|
+
const warnedOnce = new Set<string>();
|
|
6
|
+
|
|
7
|
+
function warnRestricted(key: string): void {
|
|
8
|
+
if (!warnedOnce.has(key)) {
|
|
9
|
+
warnedOnce.add(key);
|
|
10
|
+
console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildRestrictedPropSet(permissions: AssignPermissions | undefined): RestrictedPropSettingsMap | undefined {
|
|
15
|
+
const settings = permissions?.restrictedPropSettings;
|
|
16
|
+
if (!settings || settings.length === 0) return undefined;
|
|
17
|
+
const restrictedPropSet: RestrictedPropSettingsMap = new Map();
|
|
18
|
+
for (const setting of settings) {
|
|
19
|
+
const prop = typeof setting === 'string' ? setting : setting.prop;
|
|
20
|
+
if (restrictedPropSet.has(prop)) {
|
|
21
|
+
throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
|
|
22
|
+
}
|
|
23
|
+
restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
|
|
24
|
+
}
|
|
25
|
+
return restrictedPropSet;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function checkRestrictedProp(restrictedPropSet: RestrictedPropSettingsMap | undefined, key: string): boolean {
|
|
29
|
+
if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
|
|
30
|
+
warnRestricted(key);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function redirectRestrictedProp(
|
|
35
|
+
restrictedPropSet: RestrictedPropSettingsMap | undefined,
|
|
36
|
+
target: any,
|
|
37
|
+
key: string,
|
|
38
|
+
value: any
|
|
39
|
+
): boolean {
|
|
40
|
+
if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
|
|
41
|
+
const setting = restrictedPropSet.get(key);
|
|
42
|
+
if (!setting?.useMethod) {
|
|
43
|
+
warnRestricted(key);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const method = target?.[setting.useMethod];
|
|
47
|
+
if (typeof method !== 'function') {
|
|
48
|
+
warnRestricted(key);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
method.call(target, value);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/assignTentatively.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRestrictedPropSet, checkRestrictedProp } from './
|
|
1
|
+
import { buildRestrictedPropSet, checkRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
2
2
|
/**
|
|
3
3
|
* Helper function to check if a string key represents an += command
|
|
4
4
|
*/
|
package/assignTentatively.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* assignTentatively — reversible assignment with change tracking.
|
|
3
3
|
*/
|
|
4
4
|
import type { IAssignTentativelyOptions } from './types/assign-gingerly/types.js';
|
|
5
|
-
import type { AssignPermissions } from './
|
|
6
|
-
import { buildRestrictedPropSet, checkRestrictedProp } from './
|
|
5
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
6
|
+
import { buildRestrictedPropSet, checkRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
7
7
|
export type { IAssignTentativelyOptions };
|
|
8
8
|
|
|
9
9
|
/**
|
package/defineWithFeatures.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* defineWithFeatures - Declaratively define a custom element with features from JSON config.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A thin wrapper around assignFeatures: waits for the base class, creates a subclass,
|
|
5
|
+
* optionally calls onSubclassCreated, awaits assignFeatures(NewClass, config.assignFeatures),
|
|
6
|
+
* then defines the custom element in the registry.
|
|
7
7
|
*
|
|
8
8
|
* Designed to support cede scripts and other declarative custom element definition patterns.
|
|
9
9
|
*
|
|
@@ -20,30 +20,13 @@
|
|
|
20
20
|
* });
|
|
21
21
|
*/
|
|
22
22
|
import { assignFeatures } from './assignFeatures.js';
|
|
23
|
-
/**
|
|
24
|
-
* Determines if a function is an async spawner (same heuristic as assignFeatures).
|
|
25
|
-
*/
|
|
26
|
-
function isAsyncSpawn(fn) {
|
|
27
|
-
if (typeof fn !== 'function')
|
|
28
|
-
return false;
|
|
29
|
-
if (fn.constructor.name === 'AsyncFunction')
|
|
30
|
-
return true;
|
|
31
|
-
if (fn.prototype === undefined)
|
|
32
|
-
return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Cache for resolved fallback spawns.
|
|
37
|
-
* Key: BaseClass, Value: Map<featureKey, resolvedConstructor>
|
|
38
|
-
*/
|
|
39
|
-
const resolvedSpawnCache = new WeakMap();
|
|
40
23
|
/**
|
|
41
24
|
* Declaratively define a custom element with features.
|
|
42
25
|
*
|
|
43
26
|
* 1. Waits for the base class to be defined (if not already).
|
|
44
|
-
* 2.
|
|
45
|
-
* 3.
|
|
46
|
-
* 4. Calls
|
|
27
|
+
* 2. Creates a subclass extending the base class.
|
|
28
|
+
* 3. Calls the optional onSubclassCreated callback.
|
|
29
|
+
* 4. Calls assignFeatures with the JSON config and the registry's featuresRegistry.
|
|
47
30
|
* 5. Defines the new custom element in the registry.
|
|
48
31
|
*
|
|
49
32
|
* @param tagName - The custom element tag name to define (e.g., 'time-ticker')
|
|
@@ -63,62 +46,19 @@ export async function defineWithFeatures(tagName, baseTagName, config, registry,
|
|
|
63
46
|
if (!BaseClass) {
|
|
64
47
|
throw new Error(`defineWithFeatures: base class "${baseTagName}" could not be resolved`);
|
|
65
48
|
}
|
|
66
|
-
|
|
67
|
-
if (!supportedFeatures) {
|
|
68
|
-
throw new Error(`defineWithFeatures: "${baseTagName}" does not define static supportedFeatures`);
|
|
69
|
-
}
|
|
70
|
-
// 2. Resolve all async fallback spawns (with caching)
|
|
71
|
-
let classCache = resolvedSpawnCache.get(BaseClass);
|
|
72
|
-
if (!classCache) {
|
|
73
|
-
classCache = new Map();
|
|
74
|
-
resolvedSpawnCache.set(BaseClass, classCache);
|
|
75
|
-
}
|
|
76
|
-
const { assignFeatures: af } = config;
|
|
77
|
-
let resolvedSpawns;
|
|
78
|
-
if (af) {
|
|
79
|
-
const featureKeys = Object.keys(af);
|
|
80
|
-
resolvedSpawns = new Map();
|
|
81
|
-
await Promise.all(featureKeys.map(async (key) => {
|
|
82
|
-
const optIn = supportedFeatures[key];
|
|
83
|
-
if (!optIn) {
|
|
84
|
-
throw new Error(`defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`);
|
|
85
|
-
}
|
|
86
|
-
// Check cache first
|
|
87
|
-
if (classCache.has(key)) {
|
|
88
|
-
resolvedSpawns.set(key, classCache.get(key));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
let spawn = optIn.fallbackSpawn;
|
|
92
|
-
if (spawn && isAsyncSpawn(spawn)) {
|
|
93
|
-
// Resolve the async spawner
|
|
94
|
-
spawn = await spawn();
|
|
95
|
-
}
|
|
96
|
-
// Cache the resolved spawn
|
|
97
|
-
if (spawn) {
|
|
98
|
-
classCache.set(key, spawn);
|
|
99
|
-
}
|
|
100
|
-
resolvedSpawns.set(key, spawn);
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
// 3. Create subclass
|
|
49
|
+
// 2. Create subclass
|
|
104
50
|
const NewClass = class extends BaseClass {
|
|
105
51
|
};
|
|
106
|
-
//
|
|
52
|
+
// 3. Optional subclass callback
|
|
107
53
|
if (options?.onSubclassCreated) {
|
|
108
54
|
options.onSubclassCreated(NewClass);
|
|
109
55
|
}
|
|
56
|
+
// 4. Assign features (assignFeatures resolves all spawns and installs getters)
|
|
57
|
+
const { assignFeatures: af } = config;
|
|
110
58
|
if (af) {
|
|
111
|
-
|
|
112
|
-
const featuresMap = {};
|
|
113
|
-
for (const [key, jsonConfig] of Object.entries(af)) {
|
|
114
|
-
featuresMap[key] = {
|
|
115
|
-
spawn: resolvedSpawns.get(key),
|
|
116
|
-
...jsonConfig
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
// 5. assignFeatures (sequential onAssigned) + define
|
|
120
|
-
await assignFeatures(NewClass, featuresMap, reg.featuresRegistry);
|
|
59
|
+
await assignFeatures(NewClass, af, reg.featuresRegistry);
|
|
121
60
|
}
|
|
61
|
+
// 5. Define the custom element
|
|
122
62
|
reg.define(tagName, NewClass);
|
|
123
63
|
return NewClass;
|
|
124
64
|
}
|
package/defineWithFeatures.ts
CHANGED
|
@@ -1,167 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* defineWithFeatures - Declaratively define a custom element with features from JSON config.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Designed to support cede scripts and other declarative custom element definition patterns.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* await defineWithFeatures('time-ticker', 'el-maker', {
|
|
12
|
-
* assignFeatures: {
|
|
13
|
-
* timeTicker: {},
|
|
14
|
-
* roundabout: {
|
|
15
|
-
* customData: {...},
|
|
16
|
-
* withAttrs: {...},
|
|
17
|
-
* callbackForwarding: ['connectedCallback']
|
|
18
|
-
* }
|
|
19
|
-
* }
|
|
20
|
-
* });
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { assignFeatures, FeatureConfigsMap
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Configuration passed to defineWithFeatures (JSON-serializable).
|
|
27
|
-
*/
|
|
28
|
-
export interface DefineWithFeaturesConfig {
|
|
29
|
-
assignFeatures:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const supportedFeatures: SupportedFeaturesMap | undefined = BaseClass.supportedFeatures;
|
|
95
|
-
if (!supportedFeatures) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`defineWithFeatures: "${baseTagName}" does not define static supportedFeatures`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 2. Resolve all async fallback spawns (with caching)
|
|
102
|
-
let classCache = resolvedSpawnCache.get(BaseClass);
|
|
103
|
-
if (!classCache) {
|
|
104
|
-
classCache = new Map();
|
|
105
|
-
resolvedSpawnCache.set(BaseClass, classCache);
|
|
106
|
-
}
|
|
107
|
-
const {assignFeatures: af} = config;
|
|
108
|
-
let resolvedSpawns: Map<string, any> | undefined;
|
|
109
|
-
if (af) {
|
|
110
|
-
const featureKeys = Object.keys(af);
|
|
111
|
-
resolvedSpawns = new Map<string, any>();
|
|
112
|
-
|
|
113
|
-
await Promise.all(featureKeys.map(async (key) => {
|
|
114
|
-
const optIn = supportedFeatures[key];
|
|
115
|
-
if (!optIn) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Check cache first
|
|
122
|
-
if (classCache!.has(key)) {
|
|
123
|
-
resolvedSpawns!.set(key, classCache!.get(key));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let spawn = optIn.fallbackSpawn;
|
|
128
|
-
if (spawn && isAsyncSpawn(spawn)) {
|
|
129
|
-
// Resolve the async spawner
|
|
130
|
-
spawn = await (spawn as () => Promise<any>)();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Cache the resolved spawn
|
|
134
|
-
if (spawn) {
|
|
135
|
-
classCache!.set(key, spawn);
|
|
136
|
-
}
|
|
137
|
-
resolvedSpawns!.set(key, spawn);
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// 3. Create subclass
|
|
143
|
-
const NewClass = class extends (BaseClass as any) { };
|
|
144
|
-
|
|
145
|
-
// 3b. Call onSubclassCreated callback (before define, before features if needed)
|
|
146
|
-
if (options?.onSubclassCreated) {
|
|
147
|
-
options.onSubclassCreated(NewClass);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if(af){
|
|
151
|
-
// 4. Build FeatureConfigsMap: resolved spawns + JSON config
|
|
152
|
-
const featuresMap: FeatureConfigsMap = {};
|
|
153
|
-
for (const [key, jsonConfig] of Object.entries(af)) {
|
|
154
|
-
featuresMap[key] = {
|
|
155
|
-
spawn: resolvedSpawns!.get(key),
|
|
156
|
-
...jsonConfig
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 5. assignFeatures (sequential onAssigned) + define
|
|
161
|
-
await assignFeatures(NewClass, featuresMap, (reg as any).featuresRegistry);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
(reg as any).define(tagName, NewClass);
|
|
165
|
-
|
|
166
|
-
return NewClass;
|
|
167
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* defineWithFeatures - Declaratively define a custom element with features from JSON config.
|
|
3
|
+
*
|
|
4
|
+
* A thin wrapper around assignFeatures: waits for the base class, creates a subclass,
|
|
5
|
+
* optionally calls onSubclassCreated, awaits assignFeatures(NewClass, config.assignFeatures),
|
|
6
|
+
* then defines the custom element in the registry.
|
|
7
|
+
*
|
|
8
|
+
* Designed to support cede scripts and other declarative custom element definition patterns.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* await defineWithFeatures('time-ticker', 'el-maker', {
|
|
12
|
+
* assignFeatures: {
|
|
13
|
+
* timeTicker: {},
|
|
14
|
+
* roundabout: {
|
|
15
|
+
* customData: {...},
|
|
16
|
+
* withAttrs: {...},
|
|
17
|
+
* callbackForwarding: ['connectedCallback']
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { assignFeatures, FeatureConfigsMap } from './assignFeatures.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration passed to defineWithFeatures (JSON-serializable).
|
|
27
|
+
*/
|
|
28
|
+
export interface DefineWithFeaturesConfig {
|
|
29
|
+
assignFeatures: FeatureConfigsMap;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for defineWithFeatures.
|
|
34
|
+
*/
|
|
35
|
+
export interface DefineWithFeaturesOptions {
|
|
36
|
+
/** Called after the subclass is created but before registry.define(). */
|
|
37
|
+
onSubclassCreated?: (NewCtr: Function) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Declaratively define a custom element with features.
|
|
42
|
+
*
|
|
43
|
+
* 1. Waits for the base class to be defined (if not already).
|
|
44
|
+
* 2. Creates a subclass extending the base class.
|
|
45
|
+
* 3. Calls the optional onSubclassCreated callback.
|
|
46
|
+
* 4. Calls assignFeatures with the JSON config and the registry's featuresRegistry.
|
|
47
|
+
* 5. Defines the new custom element in the registry.
|
|
48
|
+
*
|
|
49
|
+
* @param tagName - The custom element tag name to define (e.g., 'time-ticker')
|
|
50
|
+
* @param baseTagName - The tag name of the base class to extend (e.g., 'el-maker')
|
|
51
|
+
* @param config - JSON-serializable configuration specifying which features to activate
|
|
52
|
+
* @param registry - Optional custom element registry (defaults to global `customElements`)
|
|
53
|
+
* @returns The newly created and defined custom element class
|
|
54
|
+
*/
|
|
55
|
+
export async function defineWithFeatures(
|
|
56
|
+
tagName: string,
|
|
57
|
+
baseTagName: string,
|
|
58
|
+
config: DefineWithFeaturesConfig,
|
|
59
|
+
registry?: CustomElementRegistry,
|
|
60
|
+
options?: DefineWithFeaturesOptions
|
|
61
|
+
): Promise<Function> {
|
|
62
|
+
const reg = registry || customElements;
|
|
63
|
+
|
|
64
|
+
// 1. Resolve base class — wait for it if not yet defined
|
|
65
|
+
let BaseClass = (reg as any).get(baseTagName);
|
|
66
|
+
if (!BaseClass) {
|
|
67
|
+
await (reg as any).whenDefined(baseTagName);
|
|
68
|
+
BaseClass = (reg as any).get(baseTagName);
|
|
69
|
+
}
|
|
70
|
+
if (!BaseClass) {
|
|
71
|
+
throw new Error(`defineWithFeatures: base class "${baseTagName}" could not be resolved`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Create subclass
|
|
75
|
+
const NewClass = class extends (BaseClass as any) { };
|
|
76
|
+
|
|
77
|
+
// 3. Optional subclass callback
|
|
78
|
+
if (options?.onSubclassCreated) {
|
|
79
|
+
options.onSubclassCreated(NewClass);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 4. Assign features (assignFeatures resolves all spawns and installs getters)
|
|
83
|
+
const { assignFeatures: af } = config;
|
|
84
|
+
if (af) {
|
|
85
|
+
await assignFeatures(NewClass, af, (reg as any).featuresRegistry);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. Define the custom element
|
|
89
|
+
(reg as any).define(tagName, NewClass);
|
|
90
|
+
|
|
91
|
+
return NewClass;
|
|
92
|
+
}
|
package/eachTime.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This module is dynamically loaded only when @eachTime is encountered
|
|
4
4
|
* Provides event-driven iteration over elements as they mount
|
|
5
5
|
*/
|
|
6
|
-
import { redirectRestrictedProp } from './
|
|
6
|
+
import { redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
7
7
|
/**
|
|
8
8
|
* Check if a value is an EventTarget
|
|
9
9
|
*/
|
package/eachTime.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { IAssignGingerlyOptions } from './types/assign-gingerly/types.js';
|
|
8
|
-
import type { AssignPermissions
|
|
9
|
-
import { redirectRestrictedProp } from './
|
|
8
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
9
|
+
import { redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
10
|
+
import type { RestrictedPropSettingsMap } from './assignPermissions/restrictedProps.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Check if a value is an EventTarget
|