@markii/pack 0.9.0 → 0.11.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/dist/components.d.ts +53 -4
- package/dist/components.js +94 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +4 -3
- package/dist/manifest.js +152 -5
- package/package.json +1 -1
package/dist/components.d.ts
CHANGED
|
@@ -18,16 +18,64 @@ export type PackComponentKind = 'inline' | 'leaf' | 'container';
|
|
|
18
18
|
* spelled differently in two places within this package and drift apart.
|
|
19
19
|
*/
|
|
20
20
|
export declare const PACK_COMPONENT_KINDS: readonly PackComponentKind[];
|
|
21
|
+
/**
|
|
22
|
+
* The charset a pack's declared attribute name must match: a leading
|
|
23
|
+
* lowercase letter, then lowercase letters, digits, and hyphens. Looser
|
|
24
|
+
* than `namespace.ts`'s `SEGMENT_RE` on purpose, because an attribute name
|
|
25
|
+
* is not a namespace: it never composes with another segment, so a
|
|
26
|
+
* trailing or doubled hyphen is ugly rather than ambiguous, and there is
|
|
27
|
+
* nothing for a stricter rule to protect.
|
|
28
|
+
*
|
|
29
|
+
* No prototype-member rejection here, unlike `validatePackName`: declared
|
|
30
|
+
* attributes travel as an ARRAY, never as an object keyed by attribute
|
|
31
|
+
* name, so an attribute called `constructor` is only ever compared as a
|
|
32
|
+
* string and can never reach a prototype chain.
|
|
33
|
+
*/
|
|
34
|
+
export declare const PACK_ATTRIBUTE_NAME_PATTERN: RegExp;
|
|
35
|
+
/** Whether `name` is a usable pack attribute name. Never throws; a non-string is simply `false`. */
|
|
36
|
+
export declare function isPackAttributeName(name: unknown): name is string;
|
|
37
|
+
/**
|
|
38
|
+
* One attribute a pack component declares in `pack.json`, so a host's
|
|
39
|
+
* completion, hover, and insert skeleton can offer it the same way they
|
|
40
|
+
* offer a standard component's contract attributes. Every field but `name`
|
|
41
|
+
* is optional: a pack that declares only names still gets attribute-name
|
|
42
|
+
* completion.
|
|
43
|
+
*
|
|
44
|
+
* This is metadata, not enforcement. Nothing validates a rendered
|
|
45
|
+
* directive's attributes against it: the renderer stays registry-driven
|
|
46
|
+
* and a component owns how it reads its own props (Architecture rules 2
|
|
47
|
+
* and 5). A `required` attribute an author leaves out is the component's
|
|
48
|
+
* problem to handle, exactly as before.
|
|
49
|
+
*/
|
|
50
|
+
export interface PackComponentAttribute {
|
|
51
|
+
/** The name an author writes inside `{...}`, e.g. `from`. Matches `PACK_ATTRIBUTE_NAME_PATTERN`. */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/** Human-readable semantics, shown in a completion row and in hover documentation. */
|
|
54
|
+
readonly description?: string;
|
|
55
|
+
/** `true` when the component is incomplete without this attribute. A required attribute is pre-filled by the insert skeleton. */
|
|
56
|
+
readonly required?: boolean;
|
|
57
|
+
/** The closed set of allowed values, when the attribute is an enum. Non-empty when present; offered as attribute-value completions. */
|
|
58
|
+
readonly values?: readonly string[];
|
|
59
|
+
/** The value used when the attribute is absent. When `values` is present, this is one of them. */
|
|
60
|
+
readonly default?: string;
|
|
61
|
+
}
|
|
21
62
|
/**
|
|
22
63
|
* The object form of a `components` manifest entry. `source` is the only
|
|
23
64
|
* required field (a pack-relative path, same meaning as the string
|
|
24
|
-
* shorthand); `description` and `
|
|
25
|
-
* surface in an insert-component catalog
|
|
65
|
+
* shorthand); `description`, `kind`, and `attributes` are optional metadata
|
|
66
|
+
* a host can surface in an insert-component catalog, a completion popup, or
|
|
67
|
+
* hover documentation.
|
|
26
68
|
*/
|
|
27
69
|
export interface PackComponentDefinition {
|
|
28
70
|
source: string;
|
|
29
71
|
description?: string;
|
|
30
72
|
kind?: PackComponentKind;
|
|
73
|
+
/**
|
|
74
|
+
* The component's declared attributes, in the order a host should offer
|
|
75
|
+
* them. Optional, and omitted rather than empty when the manifest
|
|
76
|
+
* declared none: `attributes: []` normalizes to absent.
|
|
77
|
+
*/
|
|
78
|
+
attributes?: readonly PackComponentAttribute[];
|
|
31
79
|
}
|
|
32
80
|
/**
|
|
33
81
|
* A `components` manifest entry: either the original string shorthand
|
|
@@ -37,13 +85,14 @@ export interface PackComponentDefinition {
|
|
|
37
85
|
export type PackComponentEntry = string | PackComponentDefinition;
|
|
38
86
|
/**
|
|
39
87
|
* One component's declared metadata with both entry forms normalized to
|
|
40
|
-
* the same shape. `description`/`kind` are only present when
|
|
41
|
-
* declared them (the string shorthand never has
|
|
88
|
+
* the same shape. `description`/`kind`/`attributes` are only present when
|
|
89
|
+
* the manifest declared them (the string shorthand never has any of them).
|
|
42
90
|
*/
|
|
43
91
|
export interface ResolvedPackComponent {
|
|
44
92
|
readonly source: string;
|
|
45
93
|
readonly description?: string;
|
|
46
94
|
readonly kind?: PackComponentKind;
|
|
95
|
+
readonly attributes?: readonly PackComponentAttribute[];
|
|
47
96
|
}
|
|
48
97
|
/** Same as `ResolvedPackComponent`, plus the local name it was declared under. */
|
|
49
98
|
export interface PackComponentListing extends ResolvedPackComponent {
|
package/dist/components.js
CHANGED
|
@@ -11,6 +11,93 @@ export const PACK_COMPONENT_KINDS = [
|
|
|
11
11
|
'container',
|
|
12
12
|
];
|
|
13
13
|
const PACK_COMPONENT_KIND_SET = new Set(PACK_COMPONENT_KINDS);
|
|
14
|
+
/**
|
|
15
|
+
* The charset a pack's declared attribute name must match: a leading
|
|
16
|
+
* lowercase letter, then lowercase letters, digits, and hyphens. Looser
|
|
17
|
+
* than `namespace.ts`'s `SEGMENT_RE` on purpose, because an attribute name
|
|
18
|
+
* is not a namespace: it never composes with another segment, so a
|
|
19
|
+
* trailing or doubled hyphen is ugly rather than ambiguous, and there is
|
|
20
|
+
* nothing for a stricter rule to protect.
|
|
21
|
+
*
|
|
22
|
+
* No prototype-member rejection here, unlike `validatePackName`: declared
|
|
23
|
+
* attributes travel as an ARRAY, never as an object keyed by attribute
|
|
24
|
+
* name, so an attribute called `constructor` is only ever compared as a
|
|
25
|
+
* string and can never reach a prototype chain.
|
|
26
|
+
*/
|
|
27
|
+
export const PACK_ATTRIBUTE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
28
|
+
/** Whether `name` is a usable pack attribute name. Never throws; a non-string is simply `false`. */
|
|
29
|
+
export function isPackAttributeName(name) {
|
|
30
|
+
return typeof name === 'string' && PACK_ATTRIBUTE_NAME_PATTERN.test(name);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads one own property off an already-narrowed object, or `undefined`
|
|
34
|
+
* when it is absent. `Object.hasOwn` only, matching this package's rule
|
|
35
|
+
* throughout: an inherited property is never read, so an object built with
|
|
36
|
+
* a hostile prototype cannot smuggle a value through the prototype chain.
|
|
37
|
+
*/
|
|
38
|
+
function ownProperty(entry, key) {
|
|
39
|
+
return Object.hasOwn(entry, key)
|
|
40
|
+
? entry[key]
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The lenient half of attribute handling, for `resolvePackComponent`: a
|
|
45
|
+
* malformed attribute entry is dropped rather than rejected, the same way
|
|
46
|
+
* a malformed `description` or `kind` is simply omitted here. The strict
|
|
47
|
+
* half lives in `./manifest.ts`, which rejects the whole manifest instead
|
|
48
|
+
* so a pack author sees their mistake at validation time rather than
|
|
49
|
+
* silently losing an attribute from a completion popup.
|
|
50
|
+
*/
|
|
51
|
+
function resolveAttribute(entry) {
|
|
52
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const name = ownProperty(entry, 'name');
|
|
56
|
+
if (!isPackAttributeName(name))
|
|
57
|
+
return undefined;
|
|
58
|
+
const result = { name };
|
|
59
|
+
const description = ownProperty(entry, 'description');
|
|
60
|
+
if (typeof description === 'string' && description.length > 0) {
|
|
61
|
+
result.description = description;
|
|
62
|
+
}
|
|
63
|
+
if (ownProperty(entry, 'required') === true) {
|
|
64
|
+
result.required = true;
|
|
65
|
+
}
|
|
66
|
+
const values = ownProperty(entry, 'values');
|
|
67
|
+
if (Array.isArray(values)) {
|
|
68
|
+
const cleaned = values.filter((value) => typeof value === 'string' && value.length > 0);
|
|
69
|
+
if (cleaned.length > 0)
|
|
70
|
+
result.values = cleaned;
|
|
71
|
+
}
|
|
72
|
+
const defaultValue = ownProperty(entry, 'default');
|
|
73
|
+
if (typeof defaultValue === 'string' &&
|
|
74
|
+
defaultValue.length > 0 &&
|
|
75
|
+
(result.values === undefined || result.values.includes(defaultValue))) {
|
|
76
|
+
result.default = defaultValue;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes a declared `attributes` list: keeps declaration order, drops
|
|
82
|
+
* an entry `resolveAttribute` cannot make sense of, drops a duplicate name
|
|
83
|
+
* after the first, and returns `undefined` (rather than `[]`) when nothing
|
|
84
|
+
* survives, so "declared no attributes" and "declared an empty list" are
|
|
85
|
+
* the same state everywhere downstream.
|
|
86
|
+
*/
|
|
87
|
+
function resolveAttributes(raw) {
|
|
88
|
+
if (!Array.isArray(raw))
|
|
89
|
+
return undefined;
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const resolved = [];
|
|
92
|
+
for (const entry of raw) {
|
|
93
|
+
const attribute = resolveAttribute(entry);
|
|
94
|
+
if (attribute === undefined || seen.has(attribute.name))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(attribute.name);
|
|
97
|
+
resolved.push(attribute);
|
|
98
|
+
}
|
|
99
|
+
return resolved.length > 0 ? resolved : undefined;
|
|
100
|
+
}
|
|
14
101
|
/**
|
|
15
102
|
* Normalizes a single `components` entry, string or object form, into the
|
|
16
103
|
* common `ResolvedPackComponent` shape. This is THE accessor for reading
|
|
@@ -33,30 +120,25 @@ export function resolvePackComponent(entry) {
|
|
|
33
120
|
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
34
121
|
return undefined;
|
|
35
122
|
}
|
|
36
|
-
|
|
37
|
-
// an inherited property, so an object built with a hostile prototype
|
|
38
|
-
// cannot smuggle a `source` through the prototype chain.
|
|
39
|
-
const source = Object.hasOwn(entry, 'source')
|
|
40
|
-
? entry.source
|
|
41
|
-
: undefined;
|
|
123
|
+
const source = ownProperty(entry, 'source');
|
|
42
124
|
if (typeof source !== 'string' || source.length === 0) {
|
|
43
125
|
return undefined;
|
|
44
126
|
}
|
|
45
127
|
const result = {
|
|
46
128
|
source,
|
|
47
129
|
};
|
|
48
|
-
const description =
|
|
49
|
-
? entry.description
|
|
50
|
-
: undefined;
|
|
130
|
+
const description = ownProperty(entry, 'description');
|
|
51
131
|
if (typeof description === 'string' && description.length > 0) {
|
|
52
132
|
result.description = description;
|
|
53
133
|
}
|
|
54
|
-
const kind =
|
|
55
|
-
? entry.kind
|
|
56
|
-
: undefined;
|
|
134
|
+
const kind = ownProperty(entry, 'kind');
|
|
57
135
|
if (typeof kind === 'string' && PACK_COMPONENT_KIND_SET.has(kind)) {
|
|
58
136
|
result.kind = kind;
|
|
59
137
|
}
|
|
138
|
+
const attributes = resolveAttributes(ownProperty(entry, 'attributes'));
|
|
139
|
+
if (attributes !== undefined) {
|
|
140
|
+
result.attributes = attributes;
|
|
141
|
+
}
|
|
60
142
|
return result;
|
|
61
143
|
}
|
|
62
144
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { PackManifest, PackManifestParseResult } from './manifest.js';
|
|
2
2
|
export { parsePackManifest } from './manifest.js';
|
|
3
|
-
export type { PackComponentDefinition, PackComponentEntry, PackComponentKind, PackComponentListing, ResolvedPackComponent, } from './components.js';
|
|
4
|
-
export { PACK_COMPONENT_KINDS, packComponents, resolvePackComponent, } from './components.js';
|
|
3
|
+
export type { PackComponentAttribute, PackComponentDefinition, PackComponentEntry, PackComponentKind, PackComponentListing, ResolvedPackComponent, } from './components.js';
|
|
4
|
+
export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
|
|
5
5
|
export type { ComposeDirectiveNameResult, NamespaceCollision, NamespaceValidationResult, } from './namespace.js';
|
|
6
6
|
export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
|
|
7
7
|
export type { UsesResolution } from './uses.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
// hand-rolled validation only. No registry loading, no `uses:` surfacing,
|
|
4
4
|
// no sandboxed `require`, no filesystem reads: those are later slices.
|
|
5
5
|
export { parsePackManifest } from './manifest.js';
|
|
6
|
-
export { PACK_COMPONENT_KINDS, packComponents, resolvePackComponent, } from './components.js';
|
|
6
|
+
export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
|
|
7
7
|
export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
|
|
8
8
|
export { isValidPackNameShape, resolveUses } from './uses.js';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ import type { PackComponentEntry } from './components.js';
|
|
|
11
11
|
* not maintain a closed enum of known engines. `components` maps a local
|
|
12
12
|
* component name to either a pack-relative source path (the string
|
|
13
13
|
* shorthand) or an object carrying that source plus optional `description`/
|
|
14
|
-
* `kind` metadata (see `./components.ts`,
|
|
15
|
-
* `packComponents`).
|
|
14
|
+
* `kind`/`attributes` metadata (see `./components.ts`,
|
|
15
|
+
* `resolvePackComponent`, `packComponents`).
|
|
16
16
|
*
|
|
17
17
|
* Shared Lua modules are NOT a manifest field. docs/packs.md says they
|
|
18
18
|
* travel under a conventional `scripts/` directory inside the pack
|
|
@@ -28,7 +28,8 @@ export interface PackManifest {
|
|
|
28
28
|
engine: string;
|
|
29
29
|
/**
|
|
30
30
|
* Local component name -> a pack-relative source path, or an object
|
|
31
|
-
* naming that source plus optional `description`/`kind`
|
|
31
|
+
* naming that source plus optional `description`/`kind`/`attributes`
|
|
32
|
+
* metadata. Read
|
|
32
33
|
* this map only through `resolvePackComponent`/`packComponents`
|
|
33
34
|
* (`./components.ts`) rather than inspecting entries directly — that is
|
|
34
35
|
* the one place both forms are normalized.
|
package/dist/manifest.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { validateLocalComponentName, validatePackName } from './namespace.js';
|
|
2
|
-
import { PACK_COMPONENT_KINDS } from './components.js';
|
|
2
|
+
import { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, } from './components.js';
|
|
3
3
|
const KNOWN_TOP_LEVEL_KEYS = new Set(['name', 'engine', 'components']);
|
|
4
4
|
// The keys a component's object-form entry may carry, and which of those
|
|
5
5
|
// are required. Mirrors KNOWN_TOP_LEVEL_KEYS's role for the manifest root:
|
|
6
6
|
// an unrecognized key here is a forward-compatible warning, not an error.
|
|
7
|
-
const KNOWN_COMPONENT_KEYS = new Set([
|
|
7
|
+
const KNOWN_COMPONENT_KEYS = new Set([
|
|
8
|
+
'source',
|
|
9
|
+
'description',
|
|
10
|
+
'kind',
|
|
11
|
+
'attributes',
|
|
12
|
+
]);
|
|
13
|
+
// The keys one entry of a component's `attributes` array may carry. Same
|
|
14
|
+
// posture as KNOWN_COMPONENT_KEYS one level up: an unrecognized key is a
|
|
15
|
+
// forward-compatible warning, everything recognized is validated strictly.
|
|
16
|
+
const KNOWN_ATTRIBUTE_KEYS = new Set([
|
|
17
|
+
'name',
|
|
18
|
+
'description',
|
|
19
|
+
'required',
|
|
20
|
+
'values',
|
|
21
|
+
'default',
|
|
22
|
+
]);
|
|
8
23
|
// Checked against ./components.ts's single runtime list rather than a second
|
|
9
24
|
// copy of the three values, so the validator and the accessor can never
|
|
10
25
|
// disagree about what a valid `kind` is.
|
|
@@ -12,6 +27,118 @@ const PACK_COMPONENT_KIND_SET = new Set(PACK_COMPONENT_KINDS);
|
|
|
12
27
|
function isPlainObject(value) {
|
|
13
28
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The strict half of attribute validation (the lenient half lives in
|
|
32
|
+
* `./components.ts`'s `resolveAttribute`, which a consumer may call on
|
|
33
|
+
* never-validated JSON). Anything malformed here pushes an error and
|
|
34
|
+
* rejects the WHOLE manifest, so a pack author is told what is wrong at
|
|
35
|
+
* validation time rather than quietly losing an attribute from a
|
|
36
|
+
* completion popup later.
|
|
37
|
+
*
|
|
38
|
+
* Returns the sanitized list, or `undefined` when anything failed. An
|
|
39
|
+
* empty declared list is valid and returns `[]`; the caller drops that
|
|
40
|
+
* rather than storing it, so "declared nothing" and "declared an empty
|
|
41
|
+
* list" are one state downstream.
|
|
42
|
+
*/
|
|
43
|
+
function parseComponentAttributes(componentKey, raw, errors, warnings) {
|
|
44
|
+
const parsed = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
let valid = true;
|
|
47
|
+
for (const [index, entry] of raw.entries()) {
|
|
48
|
+
const where = `"components.${componentKey}.attributes[${String(index)}]"`;
|
|
49
|
+
if (!isPlainObject(entry)) {
|
|
50
|
+
errors.push(`${where} must be an object with a "name" field`);
|
|
51
|
+
valid = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const rawName = Object.hasOwn(entry, 'name') ? entry.name : undefined;
|
|
55
|
+
if (!isPackAttributeName(rawName)) {
|
|
56
|
+
errors.push(`${where}.name must be a non-empty string matching ${PACK_ATTRIBUTE_NAME_PATTERN.source}`);
|
|
57
|
+
valid = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (seen.has(rawName)) {
|
|
61
|
+
errors.push(`${where}.name "${rawName}" is declared more than once`);
|
|
62
|
+
valid = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seen.add(rawName);
|
|
66
|
+
let entryValid = true;
|
|
67
|
+
let description;
|
|
68
|
+
if (Object.hasOwn(entry, 'description')) {
|
|
69
|
+
const rawDescription = entry.description;
|
|
70
|
+
if (typeof rawDescription !== 'string' || rawDescription.length === 0) {
|
|
71
|
+
errors.push(`${where}.description must be a non-empty string`);
|
|
72
|
+
entryValid = false;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
description = rawDescription;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let required;
|
|
79
|
+
if (Object.hasOwn(entry, 'required')) {
|
|
80
|
+
const rawRequired = entry.required;
|
|
81
|
+
if (typeof rawRequired !== 'boolean') {
|
|
82
|
+
errors.push(`${where}.required must be a boolean`);
|
|
83
|
+
entryValid = false;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
required = rawRequired;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let values;
|
|
90
|
+
if (Object.hasOwn(entry, 'values')) {
|
|
91
|
+
const rawValues = entry.values;
|
|
92
|
+
if (!Array.isArray(rawValues) ||
|
|
93
|
+
rawValues.length === 0 ||
|
|
94
|
+
rawValues.some((value) => typeof value !== 'string' || value.length === 0)) {
|
|
95
|
+
errors.push(`${where}.values must be a non-empty array of non-empty strings`);
|
|
96
|
+
entryValid = false;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
values = rawValues;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let defaultValue;
|
|
103
|
+
if (Object.hasOwn(entry, 'default')) {
|
|
104
|
+
const rawDefault = entry.default;
|
|
105
|
+
if (typeof rawDefault !== 'string' || rawDefault.length === 0) {
|
|
106
|
+
errors.push(`${where}.default must be a non-empty string`);
|
|
107
|
+
entryValid = false;
|
|
108
|
+
}
|
|
109
|
+
else if (values !== undefined && !values.includes(rawDefault)) {
|
|
110
|
+
errors.push(`${where}.default "${rawDefault}" must be one of its "values": ${values.join(', ')}`);
|
|
111
|
+
entryValid = false;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
defaultValue = rawDefault;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const attributeKey of Object.keys(entry)) {
|
|
118
|
+
if (Object.hasOwn(entry, attributeKey) &&
|
|
119
|
+
!KNOWN_ATTRIBUTE_KEYS.has(attributeKey)) {
|
|
120
|
+
warnings.push(`unknown pack component attribute key ${where}.${attributeKey} (ignored by this implementation)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!entryValid) {
|
|
124
|
+
valid = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Rebuilt rather than stored raw, the same discipline the component
|
|
128
|
+
// object form uses: only checked fields ever reach the result.
|
|
129
|
+
const sanitized = { name: rawName };
|
|
130
|
+
if (description !== undefined)
|
|
131
|
+
sanitized.description = description;
|
|
132
|
+
if (required !== undefined)
|
|
133
|
+
sanitized.required = required;
|
|
134
|
+
if (values !== undefined)
|
|
135
|
+
sanitized.values = values;
|
|
136
|
+
if (defaultValue !== undefined)
|
|
137
|
+
sanitized.default = defaultValue;
|
|
138
|
+
parsed.push(sanitized);
|
|
139
|
+
}
|
|
140
|
+
return valid ? parsed : undefined;
|
|
141
|
+
}
|
|
15
142
|
/**
|
|
16
143
|
* Hand-rolled `pack.json` validation (no schema library — see AGENTS.md
|
|
17
144
|
* dependency policy), mirroring `@markii/bundle`'s `parseManifest` style:
|
|
@@ -73,9 +200,10 @@ export function parsePackManifest(json) {
|
|
|
73
200
|
// --- components (required) ---
|
|
74
201
|
//
|
|
75
202
|
// Each entry is either the string shorthand (a pack-relative source path)
|
|
76
|
-
// or an object form { source, description?, kind? }. A
|
|
77
|
-
// wrong value type, invalid key, bad `kind`, non-string
|
|
78
|
-
// missing/empty `source` in
|
|
203
|
+
// or an object form { source, description?, kind?, attributes? }. A
|
|
204
|
+
// malformed entry — wrong value type, invalid key, bad `kind`, non-string
|
|
205
|
+
// `description`, a malformed `attributes` list, missing/empty `source` in
|
|
206
|
+
// object form — pushes an error and rejects the
|
|
79
207
|
// WHOLE manifest, matching this file's existing posture for a bad
|
|
80
208
|
// top-level field. An unrecognized key INSIDE a component object is a
|
|
81
209
|
// forward-compatible warning, the same posture KNOWN_TOP_LEVEL_KEYS uses
|
|
@@ -156,6 +284,23 @@ export function parsePackManifest(json) {
|
|
|
156
284
|
kind = rawKind;
|
|
157
285
|
}
|
|
158
286
|
}
|
|
287
|
+
let attributes;
|
|
288
|
+
if (Object.hasOwn(value, 'attributes')) {
|
|
289
|
+
const rawAttributes = value.attributes;
|
|
290
|
+
if (!Array.isArray(rawAttributes)) {
|
|
291
|
+
errors.push(`"components.${key}.attributes" must be an array of attribute objects`);
|
|
292
|
+
entryValid = false;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const parsed = parseComponentAttributes(key, rawAttributes, errors, warnings);
|
|
296
|
+
if (parsed === undefined) {
|
|
297
|
+
entryValid = false;
|
|
298
|
+
}
|
|
299
|
+
else if (parsed.length > 0) {
|
|
300
|
+
attributes = parsed;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
159
304
|
for (const componentKey of Object.keys(value)) {
|
|
160
305
|
if (Object.hasOwn(value, componentKey) &&
|
|
161
306
|
!KNOWN_COMPONENT_KEYS.has(componentKey)) {
|
|
@@ -175,6 +320,8 @@ export function parsePackManifest(json) {
|
|
|
175
320
|
sanitized.description = description;
|
|
176
321
|
if (kind !== undefined)
|
|
177
322
|
sanitized.kind = kind;
|
|
323
|
+
if (attributes !== undefined)
|
|
324
|
+
sanitized.attributes = attributes;
|
|
178
325
|
result[key] = sanitized;
|
|
179
326
|
}
|
|
180
327
|
if (Object.keys(componentsObj).length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/pack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Component pack contract for Markii (docs/packs.md): the pack.json manifest shape, namespace and engine rules, and hand-rolled validation. No React, no parsing, no bundle loading.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|