@markii/pack 0.7.3 → 0.8.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 +81 -0
- package/dist/components.js +92 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +13 -3
- package/dist/manifest.js +90 -8
- package/dist/namespace.d.ts +27 -10
- package/dist/namespace.js +28 -10
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { PackManifest } from './manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which directive form a pack component is written as. Mirrors
|
|
4
|
+
* `@markii/stdlib`'s `ComponentKind`; defined locally here rather than
|
|
5
|
+
* imported because `@markii/pack` is a published, zero-dependency package
|
|
6
|
+
* (see AGENTS.md's Stack section) and `@markii/stdlib` is a sibling
|
|
7
|
+
* package, not a dependency of this one. The two unions must stay aligned;
|
|
8
|
+
* `@markii/host` (which depends on both) carries an executable check of
|
|
9
|
+
* that alignment, since this package cannot import `@markii/stdlib` to
|
|
10
|
+
* check it itself.
|
|
11
|
+
*/
|
|
12
|
+
export type PackComponentKind = 'inline' | 'leaf' | 'container';
|
|
13
|
+
/**
|
|
14
|
+
* The three `kind` values, as runtime data. This is the ONE list: the
|
|
15
|
+
* manifest validator (`./manifest.ts`) checks a declared `kind` against it,
|
|
16
|
+
* and `@markii/host`'s alignment test compares it against
|
|
17
|
+
* `@markii/stdlib`'s `ComponentKind` members, so the values cannot be
|
|
18
|
+
* spelled differently in two places within this package and drift apart.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PACK_COMPONENT_KINDS: readonly PackComponentKind[];
|
|
21
|
+
/**
|
|
22
|
+
* The object form of a `components` manifest entry. `source` is the only
|
|
23
|
+
* required field (a pack-relative path, same meaning as the string
|
|
24
|
+
* shorthand); `description` and `kind` are optional metadata a host can
|
|
25
|
+
* surface in an insert-component catalog or similar UI.
|
|
26
|
+
*/
|
|
27
|
+
export interface PackComponentDefinition {
|
|
28
|
+
source: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
kind?: PackComponentKind;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A `components` manifest entry: either the original string shorthand
|
|
34
|
+
* (source path only) or the object form carrying optional metadata
|
|
35
|
+
* alongside the source.
|
|
36
|
+
*/
|
|
37
|
+
export type PackComponentEntry = string | PackComponentDefinition;
|
|
38
|
+
/**
|
|
39
|
+
* One component's declared metadata with both entry forms normalized to
|
|
40
|
+
* the same shape. `description`/`kind` are only present when the manifest
|
|
41
|
+
* declared them (the string shorthand never has either).
|
|
42
|
+
*/
|
|
43
|
+
export interface ResolvedPackComponent {
|
|
44
|
+
readonly source: string;
|
|
45
|
+
readonly description?: string;
|
|
46
|
+
readonly kind?: PackComponentKind;
|
|
47
|
+
}
|
|
48
|
+
/** Same as `ResolvedPackComponent`, plus the local name it was declared under. */
|
|
49
|
+
export interface PackComponentListing extends ResolvedPackComponent {
|
|
50
|
+
readonly localName: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Normalizes a single `components` entry, string or object form, into the
|
|
54
|
+
* common `ResolvedPackComponent` shape. This is THE accessor for reading
|
|
55
|
+
* one entry — callers should never inline a `typeof entry === 'string'`
|
|
56
|
+
* check of their own, so the two forms cannot silently drift apart across
|
|
57
|
+
* consumers.
|
|
58
|
+
*
|
|
59
|
+
* Never throws and never assumes the input has already been validated by
|
|
60
|
+
* `parsePackManifest`: a hostile or malformed value (not a string, not a
|
|
61
|
+
* plain object, an object missing `source`, a `source` that is not a
|
|
62
|
+
* non-empty string) returns `undefined` rather than throwing or fabricating
|
|
63
|
+
* a value. This matters because a consumer may call this directly on
|
|
64
|
+
* attacker-controlled JSON that never went through `parsePackManifest` at
|
|
65
|
+
* all (for example a pack loaded from an untrusted bundle).
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolvePackComponent(entry: unknown): ResolvedPackComponent | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* THE iterator every consumer uses instead of walking `manifest.components`
|
|
70
|
+
* directly: `Object.hasOwn`-guarded, in declaration key order, skipping any
|
|
71
|
+
* entry `resolvePackComponent` cannot resolve rather than throwing. This is
|
|
72
|
+
* the only sanctioned way to enumerate a pack's components — see
|
|
73
|
+
* `discover.ts`, `pack-build.ts`, `pack-diagnostics.ts`, and
|
|
74
|
+
* `component-catalog.ts` in `@markii/host`, all of which call this instead
|
|
75
|
+
* of re-implementing the walk.
|
|
76
|
+
*
|
|
77
|
+
* Tolerates a manifest whose `components` field is missing, `null`, not an
|
|
78
|
+
* object, or an array: all of those return `[]` rather than throwing, so a
|
|
79
|
+
* caller never needs to guard the call site itself.
|
|
80
|
+
*/
|
|
81
|
+
export declare function packComponents(manifest: Pick<PackManifest, 'components'>): PackComponentListing[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three `kind` values, as runtime data. This is the ONE list: the
|
|
3
|
+
* manifest validator (`./manifest.ts`) checks a declared `kind` against it,
|
|
4
|
+
* and `@markii/host`'s alignment test compares it against
|
|
5
|
+
* `@markii/stdlib`'s `ComponentKind` members, so the values cannot be
|
|
6
|
+
* spelled differently in two places within this package and drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export const PACK_COMPONENT_KINDS = [
|
|
9
|
+
'inline',
|
|
10
|
+
'leaf',
|
|
11
|
+
'container',
|
|
12
|
+
];
|
|
13
|
+
const PACK_COMPONENT_KIND_SET = new Set(PACK_COMPONENT_KINDS);
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes a single `components` entry, string or object form, into the
|
|
16
|
+
* common `ResolvedPackComponent` shape. This is THE accessor for reading
|
|
17
|
+
* one entry — callers should never inline a `typeof entry === 'string'`
|
|
18
|
+
* check of their own, so the two forms cannot silently drift apart across
|
|
19
|
+
* consumers.
|
|
20
|
+
*
|
|
21
|
+
* Never throws and never assumes the input has already been validated by
|
|
22
|
+
* `parsePackManifest`: a hostile or malformed value (not a string, not a
|
|
23
|
+
* plain object, an object missing `source`, a `source` that is not a
|
|
24
|
+
* non-empty string) returns `undefined` rather than throwing or fabricating
|
|
25
|
+
* a value. This matters because a consumer may call this directly on
|
|
26
|
+
* attacker-controlled JSON that never went through `parsePackManifest` at
|
|
27
|
+
* all (for example a pack loaded from an untrusted bundle).
|
|
28
|
+
*/
|
|
29
|
+
export function resolvePackComponent(entry) {
|
|
30
|
+
if (typeof entry === 'string') {
|
|
31
|
+
return entry.length > 0 ? { source: entry } : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
// Object.hasOwn only, matching this package's rule throughout: never read
|
|
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;
|
|
42
|
+
if (typeof source !== 'string' || source.length === 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const result = {
|
|
46
|
+
source,
|
|
47
|
+
};
|
|
48
|
+
const description = Object.hasOwn(entry, 'description')
|
|
49
|
+
? entry.description
|
|
50
|
+
: undefined;
|
|
51
|
+
if (typeof description === 'string' && description.length > 0) {
|
|
52
|
+
result.description = description;
|
|
53
|
+
}
|
|
54
|
+
const kind = Object.hasOwn(entry, 'kind')
|
|
55
|
+
? entry.kind
|
|
56
|
+
: undefined;
|
|
57
|
+
if (typeof kind === 'string' && PACK_COMPONENT_KIND_SET.has(kind)) {
|
|
58
|
+
result.kind = kind;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* THE iterator every consumer uses instead of walking `manifest.components`
|
|
64
|
+
* directly: `Object.hasOwn`-guarded, in declaration key order, skipping any
|
|
65
|
+
* entry `resolvePackComponent` cannot resolve rather than throwing. This is
|
|
66
|
+
* the only sanctioned way to enumerate a pack's components — see
|
|
67
|
+
* `discover.ts`, `pack-build.ts`, `pack-diagnostics.ts`, and
|
|
68
|
+
* `component-catalog.ts` in `@markii/host`, all of which call this instead
|
|
69
|
+
* of re-implementing the walk.
|
|
70
|
+
*
|
|
71
|
+
* Tolerates a manifest whose `components` field is missing, `null`, not an
|
|
72
|
+
* object, or an array: all of those return `[]` rather than throwing, so a
|
|
73
|
+
* caller never needs to guard the call site itself.
|
|
74
|
+
*/
|
|
75
|
+
export function packComponents(manifest) {
|
|
76
|
+
const components = manifest?.components;
|
|
77
|
+
if (components === null ||
|
|
78
|
+
typeof components !== 'object' ||
|
|
79
|
+
Array.isArray(components)) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const listings = [];
|
|
83
|
+
for (const localName of Object.keys(components)) {
|
|
84
|
+
if (!Object.hasOwn(components, localName))
|
|
85
|
+
continue;
|
|
86
|
+
const resolved = resolvePackComponent(components[localName]);
|
|
87
|
+
if (resolved === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
listings.push({ localName, ...resolved });
|
|
90
|
+
}
|
|
91
|
+
return listings;
|
|
92
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +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
5
|
export type { ComposeDirectiveNameResult, NamespaceCollision, NamespaceValidationResult, } from './namespace.js';
|
|
4
6
|
export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
|
|
5
7
|
export type { UsesResolution } from './uses.js';
|
package/dist/index.js
CHANGED
|
@@ -3,5 +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
7
|
export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
|
|
7
8
|
export { isValidPackNameShape, resolveUses } from './uses.js';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PackComponentEntry } from './components.js';
|
|
1
2
|
/**
|
|
2
3
|
* `pack.json`'s contract (docs/packs.md). Three fields, all required:
|
|
3
4
|
*
|
|
@@ -8,7 +9,10 @@
|
|
|
8
9
|
* renderer framework the pack's components are written for; it is any
|
|
9
10
|
* string — the host decides whether it can run it, so this package does
|
|
10
11
|
* not maintain a closed enum of known engines. `components` maps a local
|
|
11
|
-
* component name to a pack-relative source path
|
|
12
|
+
* component name to either a pack-relative source path (the string
|
|
13
|
+
* shorthand) or an object carrying that source plus optional `description`/
|
|
14
|
+
* `kind` metadata (see `./components.ts`, `resolvePackComponent`,
|
|
15
|
+
* `packComponents`).
|
|
12
16
|
*
|
|
13
17
|
* Shared Lua modules are NOT a manifest field. docs/packs.md says they
|
|
14
18
|
* travel under a conventional `scripts/` directory inside the pack
|
|
@@ -22,8 +26,14 @@ export interface PackManifest {
|
|
|
22
26
|
name: string;
|
|
23
27
|
/** The renderer framework the pack's components target, e.g. `"react"`. */
|
|
24
28
|
engine: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Local component name -> a pack-relative source path, or an object
|
|
31
|
+
* naming that source plus optional `description`/`kind` metadata. Read
|
|
32
|
+
* this map only through `resolvePackComponent`/`packComponents`
|
|
33
|
+
* (`./components.ts`) rather than inspecting entries directly — that is
|
|
34
|
+
* the one place both forms are normalized.
|
|
35
|
+
*/
|
|
36
|
+
components: Record<string, PackComponentEntry>;
|
|
27
37
|
}
|
|
28
38
|
export type PackManifestParseResult = {
|
|
29
39
|
ok: true;
|
package/dist/manifest.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { validateLocalComponentName, validatePackName } from './namespace.js';
|
|
2
|
+
import { PACK_COMPONENT_KINDS } from './components.js';
|
|
2
3
|
const KNOWN_TOP_LEVEL_KEYS = new Set(['name', 'engine', 'components']);
|
|
4
|
+
// The keys a component's object-form entry may carry, and which of those
|
|
5
|
+
// are required. Mirrors KNOWN_TOP_LEVEL_KEYS's role for the manifest root:
|
|
6
|
+
// an unrecognized key here is a forward-compatible warning, not an error.
|
|
7
|
+
const KNOWN_COMPONENT_KEYS = new Set(['source', 'description', 'kind']);
|
|
8
|
+
// Checked against ./components.ts's single runtime list rather than a second
|
|
9
|
+
// copy of the three values, so the validator and the accessor can never
|
|
10
|
+
// disagree about what a valid `kind` is.
|
|
11
|
+
const PACK_COMPONENT_KIND_SET = new Set(PACK_COMPONENT_KINDS);
|
|
3
12
|
function isPlainObject(value) {
|
|
4
13
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
14
|
}
|
|
@@ -62,6 +71,18 @@ export function parsePackManifest(json) {
|
|
|
62
71
|
engine = obj.engine;
|
|
63
72
|
}
|
|
64
73
|
// --- components (required) ---
|
|
74
|
+
//
|
|
75
|
+
// Each entry is either the string shorthand (a pack-relative source path)
|
|
76
|
+
// or an object form { source, description?, kind? }. A malformed entry —
|
|
77
|
+
// wrong value type, invalid key, bad `kind`, non-string `description`,
|
|
78
|
+
// missing/empty `source` in object form — pushes an error and rejects the
|
|
79
|
+
// WHOLE manifest, matching this file's existing posture for a bad
|
|
80
|
+
// top-level field. An unrecognized key INSIDE a component object is a
|
|
81
|
+
// forward-compatible warning, the same posture KNOWN_TOP_LEVEL_KEYS uses
|
|
82
|
+
// for the manifest root. Only the checked, sanitized fields are ever
|
|
83
|
+
// copied into `result`/the rebuilt component object — the raw parsed
|
|
84
|
+
// value is never stored, so an unknown or hostile key on a component
|
|
85
|
+
// object can never surface later even as inert data.
|
|
65
86
|
let components;
|
|
66
87
|
if (!Object.hasOwn(obj, 'components')) {
|
|
67
88
|
errors.push('"components" is required');
|
|
@@ -83,17 +104,78 @@ export function parsePackManifest(json) {
|
|
|
83
104
|
continue;
|
|
84
105
|
}
|
|
85
106
|
const value = componentsObj[key];
|
|
86
|
-
if (typeof value
|
|
87
|
-
|
|
107
|
+
if (typeof value === 'string') {
|
|
108
|
+
if (value.length === 0) {
|
|
109
|
+
errors.push(`"components.${key}" must be a non-empty string (a pack-relative source path) or an object with a "source" field`);
|
|
110
|
+
componentsValid = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Only the type is checked here, matching @markii/bundle's
|
|
114
|
+
// `document` field: path-jailing a pack-relative source path is a
|
|
115
|
+
// filesystem concern for the later registry-loading slice, not
|
|
116
|
+
// this contract-only one. Object.hasOwn above already keeps
|
|
117
|
+
// prototype members out of `result`.
|
|
118
|
+
result[key] = value;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!isPlainObject(value)) {
|
|
122
|
+
errors.push(`"components.${key}" must be a non-empty string (a pack-relative source path) or an object with a "source" field`);
|
|
123
|
+
componentsValid = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Object form: validate source (required), description (optional),
|
|
127
|
+
// kind (optional), and rebuild a sanitized object rather than storing
|
|
128
|
+
// the raw parsed one — the same discipline as the string-form branch
|
|
129
|
+
// and as the top-level Object.hasOwn guarding throughout this file.
|
|
130
|
+
let entryValid = true;
|
|
131
|
+
const source = Object.hasOwn(value, 'source') ? value.source : undefined;
|
|
132
|
+
if (typeof source !== 'string' || source.length === 0) {
|
|
133
|
+
errors.push(`"components.${key}.source" must be a non-empty string (a pack-relative source path)`);
|
|
134
|
+
entryValid = false;
|
|
135
|
+
}
|
|
136
|
+
let description;
|
|
137
|
+
if (Object.hasOwn(value, 'description')) {
|
|
138
|
+
const rawDescription = value.description;
|
|
139
|
+
if (typeof rawDescription !== 'string' || rawDescription.length === 0) {
|
|
140
|
+
errors.push(`"components.${key}.description" must be a non-empty string`);
|
|
141
|
+
entryValid = false;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
description = rawDescription;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let kind;
|
|
148
|
+
if (Object.hasOwn(value, 'kind')) {
|
|
149
|
+
const rawKind = value.kind;
|
|
150
|
+
if (typeof rawKind !== 'string' ||
|
|
151
|
+
!PACK_COMPONENT_KIND_SET.has(rawKind)) {
|
|
152
|
+
errors.push(`"components.${key}.kind" must be one of "inline", "leaf", "container"`);
|
|
153
|
+
entryValid = false;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
kind = rawKind;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const componentKey of Object.keys(value)) {
|
|
160
|
+
if (Object.hasOwn(value, componentKey) &&
|
|
161
|
+
!KNOWN_COMPONENT_KEYS.has(componentKey)) {
|
|
162
|
+
warnings.push(`unknown pack component key "components.${key}.${componentKey}" (ignored by this implementation)`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!entryValid) {
|
|
88
166
|
componentsValid = false;
|
|
89
167
|
continue;
|
|
90
168
|
}
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
169
|
+
// The object form is kept as an object even when neither optional
|
|
170
|
+
// field was present — only the string shorthand collapses to a bare
|
|
171
|
+
// string. This preserves the author's declared form rather than
|
|
172
|
+
// silently normalizing `{ "source": "./x.tsx" }` into `"./x.tsx"`.
|
|
173
|
+
const sanitized = { source: source };
|
|
174
|
+
if (description !== undefined)
|
|
175
|
+
sanitized.description = description;
|
|
176
|
+
if (kind !== undefined)
|
|
177
|
+
sanitized.kind = kind;
|
|
178
|
+
result[key] = sanitized;
|
|
97
179
|
}
|
|
98
180
|
if (Object.keys(componentsObj).length === 0) {
|
|
99
181
|
errors.push('"components" must have at least one entry');
|
package/dist/namespace.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - Directive names MUST NOT contain `:` (reserved syntax).
|
|
5
5
|
* - A namespaced directive name joins the pack's namespace and the
|
|
6
|
-
* component's local name with
|
|
6
|
+
* component's local name with `_`, and only `_` (issue #19). The spec
|
|
7
|
+
* permits `-` or `_` for a directive name generally; this package composes
|
|
8
|
+
* exactly one of them, so the boundary between the two parts is always
|
|
9
|
+
* unambiguous. See `composeDirectiveName` for why.
|
|
7
10
|
* - The bundle's reserved first-path-segment names (`scripts`, `assets`,
|
|
8
11
|
* `.cache`) can never be a pack namespace, so a pack can never shadow
|
|
9
12
|
* bundle structure in `require` or in directive names.
|
|
@@ -20,11 +23,11 @@
|
|
|
20
23
|
* hyphens as separators (no leading/trailing hyphen, no doubled hyphen).
|
|
21
24
|
* This is deliberately conservative — it's stricter than the spec's bare
|
|
22
25
|
* "MUST NOT contain `:`" requirement — because a namespace also has to
|
|
23
|
-
* compose cleanly with a local name
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* compose cleanly with a local name without producing an ambiguous or
|
|
27
|
+
* surprising directive name. Underscore is the composition separator
|
|
28
|
+
* (`ana_timeline`) and is deliberately NOT accepted inside the namespace or
|
|
29
|
+
* local-name segments themselves, which is exactly what makes the composed
|
|
30
|
+
* name's single underscore mark the boundary unambiguously.
|
|
28
31
|
*/
|
|
29
32
|
/** The reserved bundle directory names a pack namespace must never collide with (spec.md §1, §8). */
|
|
30
33
|
export declare const RESERVED_NAMESPACE_SEGMENTS: ReadonlySet<string>;
|
|
@@ -58,15 +61,29 @@ export type ComposeDirectiveNameResult = {
|
|
|
58
61
|
};
|
|
59
62
|
/**
|
|
60
63
|
* Composes the directive name an author types to reference a pack's
|
|
61
|
-
* component, e.g. `composeDirectiveName('ana', 'timeline')` -> `
|
|
64
|
+
* component, e.g. `composeDirectiveName('ana', 'timeline')` -> `ana_timeline`
|
|
62
65
|
* (docs/packs.md). Validates both inputs first, then the composed result
|
|
63
66
|
* (defense in depth: the composed string must still contain no `:` and must
|
|
64
67
|
* still be a legal directive-name shape).
|
|
65
68
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
69
|
+
* The separator is `_`, and there is deliberately no way to ask for another
|
|
70
|
+
* one (issue #19, user-settled 2026-08-30). It used to be `-`, which is also
|
|
71
|
+
* the separator INSIDE a lowercase-kebab pack or local name, so two
|
|
72
|
+
* different packs could compose the same directive name: pack `long` plus
|
|
73
|
+
* component `pack-name-some-component` and pack `long-pack-name` plus
|
|
74
|
+
* component `some-component` both produced `long-pack-name-some-component`,
|
|
75
|
+
* and one silently shadowed the other. `SEGMENT_RE` bans `_` inside either
|
|
76
|
+
* segment, so an underscore join gives every composed name exactly one
|
|
77
|
+
* underscore, at the pack/component boundary. That makes the split
|
|
78
|
+
* bijective and this whole collision class impossible to construct rather
|
|
79
|
+
* than merely detected.
|
|
80
|
+
*
|
|
81
|
+
* Accepting both forms would have kept the ambiguity alive, so only this one
|
|
82
|
+
* exists. Notes and packs written against the old `-` composition stop
|
|
83
|
+
* resolving and render the labeled unknown-component fallback; that breakage
|
|
84
|
+
* was weighed and accepted pre-1.0 rather than carrying a migration shim.
|
|
68
85
|
*/
|
|
69
|
-
export declare function composeDirectiveName(packName: string, localName: string
|
|
86
|
+
export declare function composeDirectiveName(packName: string, localName: string): ComposeDirectiveNameResult;
|
|
70
87
|
export interface NamespaceCollision {
|
|
71
88
|
namespace: string;
|
|
72
89
|
/** How many times this namespace appears in the input. */
|
package/dist/namespace.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - Directive names MUST NOT contain `:` (reserved syntax).
|
|
5
5
|
* - A namespaced directive name joins the pack's namespace and the
|
|
6
|
-
* component's local name with
|
|
6
|
+
* component's local name with `_`, and only `_` (issue #19). The spec
|
|
7
|
+
* permits `-` or `_` for a directive name generally; this package composes
|
|
8
|
+
* exactly one of them, so the boundary between the two parts is always
|
|
9
|
+
* unambiguous. See `composeDirectiveName` for why.
|
|
7
10
|
* - The bundle's reserved first-path-segment names (`scripts`, `assets`,
|
|
8
11
|
* `.cache`) can never be a pack namespace, so a pack can never shadow
|
|
9
12
|
* bundle structure in `require` or in directive names.
|
|
@@ -20,11 +23,11 @@
|
|
|
20
23
|
* hyphens as separators (no leading/trailing hyphen, no doubled hyphen).
|
|
21
24
|
* This is deliberately conservative — it's stricter than the spec's bare
|
|
22
25
|
* "MUST NOT contain `:`" requirement — because a namespace also has to
|
|
23
|
-
* compose cleanly with a local name
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* compose cleanly with a local name without producing an ambiguous or
|
|
27
|
+
* surprising directive name. Underscore is the composition separator
|
|
28
|
+
* (`ana_timeline`) and is deliberately NOT accepted inside the namespace or
|
|
29
|
+
* local-name segments themselves, which is exactly what makes the composed
|
|
30
|
+
* name's single underscore mark the boundary unambiguously.
|
|
28
31
|
*/
|
|
29
32
|
/** The reserved bundle directory names a pack namespace must never collide with (spec.md §1, §8). */
|
|
30
33
|
export const RESERVED_NAMESPACE_SEGMENTS = new Set([
|
|
@@ -116,15 +119,30 @@ export function validateLocalComponentName(name) {
|
|
|
116
119
|
}
|
|
117
120
|
/**
|
|
118
121
|
* Composes the directive name an author types to reference a pack's
|
|
119
|
-
* component, e.g. `composeDirectiveName('ana', 'timeline')` -> `
|
|
122
|
+
* component, e.g. `composeDirectiveName('ana', 'timeline')` -> `ana_timeline`
|
|
120
123
|
* (docs/packs.md). Validates both inputs first, then the composed result
|
|
121
124
|
* (defense in depth: the composed string must still contain no `:` and must
|
|
122
125
|
* still be a legal directive-name shape).
|
|
123
126
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
127
|
+
* The separator is `_`, and there is deliberately no way to ask for another
|
|
128
|
+
* one (issue #19, user-settled 2026-08-30). It used to be `-`, which is also
|
|
129
|
+
* the separator INSIDE a lowercase-kebab pack or local name, so two
|
|
130
|
+
* different packs could compose the same directive name: pack `long` plus
|
|
131
|
+
* component `pack-name-some-component` and pack `long-pack-name` plus
|
|
132
|
+
* component `some-component` both produced `long-pack-name-some-component`,
|
|
133
|
+
* and one silently shadowed the other. `SEGMENT_RE` bans `_` inside either
|
|
134
|
+
* segment, so an underscore join gives every composed name exactly one
|
|
135
|
+
* underscore, at the pack/component boundary. That makes the split
|
|
136
|
+
* bijective and this whole collision class impossible to construct rather
|
|
137
|
+
* than merely detected.
|
|
138
|
+
*
|
|
139
|
+
* Accepting both forms would have kept the ambiguity alive, so only this one
|
|
140
|
+
* exists. Notes and packs written against the old `-` composition stop
|
|
141
|
+
* resolving and render the labeled unknown-component fallback; that breakage
|
|
142
|
+
* was weighed and accepted pre-1.0 rather than carrying a migration shim.
|
|
126
143
|
*/
|
|
127
|
-
export function composeDirectiveName(packName, localName
|
|
144
|
+
export function composeDirectiveName(packName, localName) {
|
|
145
|
+
const separator = '_';
|
|
128
146
|
const packResult = validatePackName(packName);
|
|
129
147
|
if (!packResult.ok)
|
|
130
148
|
return packResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/pack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|