@markii/pack 0.10.0 → 0.12.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/archive.d.ts +81 -0
- package/dist/archive.js +200 -0
- package/dist/components.d.ts +7 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -3
- package/dist/manifest.d.ts +27 -2
- package/dist/manifest.js +34 -7
- package/dist/namespace.d.ts +4 -3
- package/dist/namespace.js +4 -3
- package/package.json +5 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { PackManifest } from './manifest.js';
|
|
2
|
+
/** Options bounding how much a single `.mkp` open is willing to materialize. Defaults match `@markii/bundle`'s own zip-bomb guard, since a pack archive is bounded the same way a bundle is. */
|
|
3
|
+
export interface OpenPackArchiveOptions {
|
|
4
|
+
maxEntryBytes?: number;
|
|
5
|
+
maxTotalBytes?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Why an `openPackArchive` call failed. `kind: 'zip'` covers everything
|
|
9
|
+
* `openZipBundle` itself rejects (a path-escaping entry, an oversized
|
|
10
|
+
* entry, a corrupt or malformed zip, ZIP64, a CRC mismatch, a name
|
|
11
|
+
* collision) — the message is already loud and specific, so it is passed
|
|
12
|
+
* through rather than re-summarized. `kind: 'manifest'` covers a
|
|
13
|
+
* `pack.json` that parses to invalid JSON or fails `parsePackManifest`.
|
|
14
|
+
* `kind: 'missing-entry'` covers a `.mkp` that lacks a required root entry.
|
|
15
|
+
*/
|
|
16
|
+
export type PackArchiveError = {
|
|
17
|
+
kind: 'zip';
|
|
18
|
+
message: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'manifest';
|
|
21
|
+
errors: string[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'missing-entry';
|
|
24
|
+
entry: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
export type OpenPackArchiveResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
archive: PackArchiveContents;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
error: PackArchiveError;
|
|
33
|
+
};
|
|
34
|
+
/** The validated, read-only contents of a `.mkp` archive. */
|
|
35
|
+
export interface PackArchiveContents {
|
|
36
|
+
/** The parsed, validated `pack.json`. */
|
|
37
|
+
readonly manifest: PackManifest;
|
|
38
|
+
/** Forward-compatible warnings from parsing `pack.json` (unknown keys etc.) — see `parsePackManifest`. */
|
|
39
|
+
readonly manifestWarnings: readonly string[];
|
|
40
|
+
/** The prebuilt registration script's bytes (`webview.js`, always present — see this module's doc comment). */
|
|
41
|
+
readonly scriptBytes: Uint8Array;
|
|
42
|
+
/** The prebuilt stylesheet's bytes (`webview.css`), present only when the pack ships one. */
|
|
43
|
+
readonly stylesheetBytes?: Uint8Array;
|
|
44
|
+
/** Shared Lua modules under `scripts/`, keyed by their path relative to that folder (e.g. `"http.lua"` for `scripts/http.lua`). Empty when the pack ships none. */
|
|
45
|
+
readonly scriptModules: Readonly<Record<string, Uint8Array>>;
|
|
46
|
+
/**
|
|
47
|
+
* Archive-root entries present in the zip but outside the prebuilt
|
|
48
|
+
* contract (e.g. a leftover component source alongside the compiled
|
|
49
|
+
* script, or an entry nested under a subfolder rather than at the zip
|
|
50
|
+
* root). Never an error: `.mkp` follows the same "prebuilt wins,
|
|
51
|
+
* anything else is ignored" posture a pack folder already has for a
|
|
52
|
+
* `webview.js` sitting beside sources. A host's diagnostics surface may
|
|
53
|
+
* report these; this module only supplies the facts.
|
|
54
|
+
*/
|
|
55
|
+
readonly ignoredEntries: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opens a `.mkp` archive from its raw bytes and validates it against the
|
|
59
|
+
* prebuilt-pack contract. Never throws: every failure — a hostile or
|
|
60
|
+
* corrupt zip, a missing required entry, an invalid `pack.json` — comes
|
|
61
|
+
* back as `{ ok: false, error }` rather than an exception, so a caller can
|
|
62
|
+
* report the reason without a `try`/`catch` of its own.
|
|
63
|
+
*/
|
|
64
|
+
export declare function openPackArchive(bytes: Uint8Array, options?: OpenPackArchiveOptions): Promise<OpenPackArchiveResult>;
|
|
65
|
+
/**
|
|
66
|
+
* The archive naming rule for a PRODUCED `.mkp`: `<name>-<version>.mkp`
|
|
67
|
+
* when `pack.json` declares a `version`, or `<name>.mkp` when it does not.
|
|
68
|
+
*
|
|
69
|
+
* `version` is optional in the manifest (see `PackManifest`'s doc comment)
|
|
70
|
+
* and its absence is valid, not an error — so a produced archive's name
|
|
71
|
+
* must stay valid too. Falling back to the bare pack name rather than
|
|
72
|
+
* inventing a placeholder version (`0.0.0`, `unversioned`, ...) avoids
|
|
73
|
+
* claiming a version the manifest never declared: the filename says exactly
|
|
74
|
+
* as much as the manifest does, no more.
|
|
75
|
+
*
|
|
76
|
+
* Both `name` and a present `version` are already validated by
|
|
77
|
+
* `parsePackManifest` (lowercase-kebab; plain `MAJOR.MINOR.PATCH` digits
|
|
78
|
+
* and dots) before this function ever sees them, so no further escaping is
|
|
79
|
+
* needed to use them as a filename.
|
|
80
|
+
*/
|
|
81
|
+
export declare function packArchiveFileName(manifest: Pick<PackManifest, 'name' | 'version'>): string;
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.mkp` pack archives (docs/packs.md, issue #16): a zip of a pack's
|
|
3
|
+
* PREBUILT form, files at the zip root — `pack.json`, `webview.js`,
|
|
4
|
+
* `webview.css` when the pack has styles, `scripts/` when the pack ships
|
|
5
|
+
* shared Lua. This is the reader half: given the raw bytes of a `.mkp`
|
|
6
|
+
* file, validate it and expose its contents for read-only loading.
|
|
7
|
+
*
|
|
8
|
+
* A `.mkp` is prebuilt-only, on purpose (docs/packs.md's "Two ways to run a
|
|
9
|
+
* pack: prebuilt and from source"): this module never compiles anything. A
|
|
10
|
+
* `webview.js` at the archive root is REQUIRED — an archive that ships only
|
|
11
|
+
* component sources is not a valid `.mkp`, because there would be nothing
|
|
12
|
+
* for a host to load without running a compiler, and `.mkp` exists
|
|
13
|
+
* precisely for hosts that may not carry one.
|
|
14
|
+
*
|
|
15
|
+
* This reader reuses `@markii/bundle`'s zip reader (`openZipBundle`)
|
|
16
|
+
* rather than re-implementing zip parsing or a path jail: AGENTS.md calls a
|
|
17
|
+
* reimplemented jail a bug. `openZipBundle` already rejects a `../`
|
|
18
|
+
* segment, an absolute path, a backslash path, or a Windows drive-letter
|
|
19
|
+
* path in any entry name (zip-slip), and bounds both a single entry's and
|
|
20
|
+
* the whole archive's declared uncompressed size before any bytes are
|
|
21
|
+
* inflated (the zip-bomb guard) — see `packages/markii-bundle/src/zip.ts`.
|
|
22
|
+
* Reusing it means a `.mkp` gets the exact same `bounded-open` guarantee a
|
|
23
|
+
* `.mkz` bundle gets, with no second implementation to drift out of sync.
|
|
24
|
+
*
|
|
25
|
+
* This module never touches a real filesystem: it takes bytes in and
|
|
26
|
+
* returns parsed, in-memory contents (or a structured error) out. It never
|
|
27
|
+
* writes anything anywhere, so "nothing is written outside the pack
|
|
28
|
+
* directory" holds structurally for every rejection path — there is no
|
|
29
|
+
* write side to leak from. A host that wants to install a `.mkp` (unzip it
|
|
30
|
+
* into its own pack directory) is a separate, host-specific concern (the
|
|
31
|
+
* brief's "Install pack from file" command) and is out of this module's
|
|
32
|
+
* scope entirely.
|
|
33
|
+
*/
|
|
34
|
+
import { BundleZipError, DEFAULT_MAX_ZIP_ENTRY_BYTES, DEFAULT_MAX_ZIP_TOTAL_BYTES, openZipBundle, } from '@markii/bundle';
|
|
35
|
+
import { parsePackManifest } from './manifest.js';
|
|
36
|
+
/** The archive-root entry names the prebuilt contract reserves. */
|
|
37
|
+
const MANIFEST_ENTRY = 'pack.json';
|
|
38
|
+
const SCRIPT_ENTRY = 'webview.js';
|
|
39
|
+
const STYLESHEET_ENTRY = 'webview.css';
|
|
40
|
+
const SCRIPTS_DIR_PREFIX = 'scripts/';
|
|
41
|
+
function isScriptModulePath(path) {
|
|
42
|
+
return (path.startsWith(SCRIPTS_DIR_PREFIX) &&
|
|
43
|
+
path.length > SCRIPTS_DIR_PREFIX.length);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decodes UTF-8 bytes, rejecting anything that is not well-formed UTF-8
|
|
47
|
+
* rather than silently substituting replacement characters (the default
|
|
48
|
+
* `TextDecoder` behavior) — a `pack.json` that decodes to mangled text
|
|
49
|
+
* should fail loudly as a manifest problem, not parse whatever garbled JSON
|
|
50
|
+
* `�` substitution happens to produce.
|
|
51
|
+
*/
|
|
52
|
+
function decodeUtf8Strict(bytes) {
|
|
53
|
+
try {
|
|
54
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Opens a `.mkp` archive from its raw bytes and validates it against the
|
|
62
|
+
* prebuilt-pack contract. Never throws: every failure — a hostile or
|
|
63
|
+
* corrupt zip, a missing required entry, an invalid `pack.json` — comes
|
|
64
|
+
* back as `{ ok: false, error }` rather than an exception, so a caller can
|
|
65
|
+
* report the reason without a `try`/`catch` of its own.
|
|
66
|
+
*/
|
|
67
|
+
export async function openPackArchive(bytes, options = {}) {
|
|
68
|
+
const maxEntryBytes = options.maxEntryBytes ?? DEFAULT_MAX_ZIP_ENTRY_BYTES;
|
|
69
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_ZIP_TOTAL_BYTES;
|
|
70
|
+
let storage;
|
|
71
|
+
try {
|
|
72
|
+
storage = openZipBundle(bytes, { maxEntryBytes, maxTotalBytes });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof BundleZipError) {
|
|
76
|
+
return { ok: false, error: { kind: 'zip', message: err.message } };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: {
|
|
81
|
+
kind: 'zip',
|
|
82
|
+
message: `pack archive rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const paths = await storage.list();
|
|
87
|
+
if (!paths.includes(MANIFEST_ENTRY)) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: {
|
|
91
|
+
kind: 'missing-entry',
|
|
92
|
+
entry: MANIFEST_ENTRY,
|
|
93
|
+
message: `pack archive rejected: no "${MANIFEST_ENTRY}" entry at the archive root`,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!paths.includes(SCRIPT_ENTRY)) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: {
|
|
101
|
+
kind: 'missing-entry',
|
|
102
|
+
entry: SCRIPT_ENTRY,
|
|
103
|
+
message: `pack archive rejected: no "${SCRIPT_ENTRY}" entry at the archive root: a .mkp carries a pack's prebuilt form only, never its sources, so a compiled ${SCRIPT_ENTRY} is required`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const manifestBytes = await storage.read(MANIFEST_ENTRY);
|
|
108
|
+
// Not reachable in practice (the entry was just confirmed present in the
|
|
109
|
+
// same storage instance's own listing), but `read` is typed to return
|
|
110
|
+
// `undefined` and this keeps the function honestly total rather than
|
|
111
|
+
// asserting past the type.
|
|
112
|
+
if (manifestBytes === undefined) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: {
|
|
116
|
+
kind: 'missing-entry',
|
|
117
|
+
entry: MANIFEST_ENTRY,
|
|
118
|
+
message: `pack archive rejected: "${MANIFEST_ENTRY}" could not be read`,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const manifestText = decodeUtf8Strict(manifestBytes);
|
|
123
|
+
if (manifestText === undefined) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: {
|
|
127
|
+
kind: 'manifest',
|
|
128
|
+
errors: [`"${MANIFEST_ENTRY}" is not valid UTF-8`],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const parsed = parsePackManifest(manifestText);
|
|
133
|
+
if (!parsed.ok) {
|
|
134
|
+
return { ok: false, error: { kind: 'manifest', errors: parsed.errors } };
|
|
135
|
+
}
|
|
136
|
+
const scriptBytes = await storage.read(SCRIPT_ENTRY);
|
|
137
|
+
if (scriptBytes === undefined) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: {
|
|
141
|
+
kind: 'missing-entry',
|
|
142
|
+
entry: SCRIPT_ENTRY,
|
|
143
|
+
message: `pack archive rejected: "${SCRIPT_ENTRY}" could not be read`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
let stylesheetBytes;
|
|
148
|
+
if (paths.includes(STYLESHEET_ENTRY)) {
|
|
149
|
+
stylesheetBytes = await storage.read(STYLESHEET_ENTRY);
|
|
150
|
+
}
|
|
151
|
+
const scriptModules = Object.create(null);
|
|
152
|
+
const ignoredEntries = [];
|
|
153
|
+
for (const path of paths) {
|
|
154
|
+
if (path === MANIFEST_ENTRY ||
|
|
155
|
+
path === SCRIPT_ENTRY ||
|
|
156
|
+
path === STYLESHEET_ENTRY) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (isScriptModulePath(path)) {
|
|
160
|
+
const data = await storage.read(path);
|
|
161
|
+
if (data !== undefined) {
|
|
162
|
+
scriptModules[path.slice(SCRIPTS_DIR_PREFIX.length)] = data;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
ignoredEntries.push(path);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
archive: {
|
|
171
|
+
manifest: parsed.manifest,
|
|
172
|
+
manifestWarnings: parsed.warnings,
|
|
173
|
+
scriptBytes,
|
|
174
|
+
...(stylesheetBytes !== undefined ? { stylesheetBytes } : {}),
|
|
175
|
+
scriptModules,
|
|
176
|
+
ignoredEntries,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The archive naming rule for a PRODUCED `.mkp`: `<name>-<version>.mkp`
|
|
182
|
+
* when `pack.json` declares a `version`, or `<name>.mkp` when it does not.
|
|
183
|
+
*
|
|
184
|
+
* `version` is optional in the manifest (see `PackManifest`'s doc comment)
|
|
185
|
+
* and its absence is valid, not an error — so a produced archive's name
|
|
186
|
+
* must stay valid too. Falling back to the bare pack name rather than
|
|
187
|
+
* inventing a placeholder version (`0.0.0`, `unversioned`, ...) avoids
|
|
188
|
+
* claiming a version the manifest never declared: the filename says exactly
|
|
189
|
+
* as much as the manifest does, no more.
|
|
190
|
+
*
|
|
191
|
+
* Both `name` and a present `version` are already validated by
|
|
192
|
+
* `parsePackManifest` (lowercase-kebab; plain `MAJOR.MINOR.PATCH` digits
|
|
193
|
+
* and dots) before this function ever sees them, so no further escaping is
|
|
194
|
+
* needed to use them as a filename.
|
|
195
|
+
*/
|
|
196
|
+
export function packArchiveFileName(manifest) {
|
|
197
|
+
return manifest.version === undefined
|
|
198
|
+
? `${manifest.name}.mkp`
|
|
199
|
+
: `${manifest.name}-${manifest.version}.mkp`;
|
|
200
|
+
}
|
package/dist/components.d.ts
CHANGED
|
@@ -2,9 +2,13 @@ import type { PackManifest } from './manifest.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Which directive form a pack component is written as. Mirrors
|
|
4
4
|
* `@markii/stdlib`'s `ComponentKind`; defined locally here rather than
|
|
5
|
-
* imported because `@markii/
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* imported because `@markii/stdlib` is a sibling package this package
|
|
6
|
+
* deliberately does not depend on, to stay decoupled from the reference
|
|
7
|
+
* renderer's component contracts (see AGENTS.md's Stack section). This is
|
|
8
|
+
* unrelated to `@markii/pack`'s dependency on `@markii/bundle` for `.mkp`
|
|
9
|
+
* archive reading (`./archive.ts`, issue #16): that dependency reuses a
|
|
10
|
+
* neutral, framework-agnostic zip reader and path jail, not a renderer
|
|
11
|
+
* contract. The two unions must stay aligned;
|
|
8
12
|
* `@markii/host` (which depends on both) carries an executable check of
|
|
9
13
|
* that alignment, since this package cannot import `@markii/stdlib` to
|
|
10
14
|
* check it itself.
|
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 { OpenPackArchiveOptions, OpenPackArchiveResult, PackArchiveContents, PackArchiveError, } from './archive.js';
|
|
4
|
+
export { openPackArchive, packArchiveFileName } from './archive.js';
|
|
3
5
|
export type { PackComponentAttribute, PackComponentDefinition, PackComponentEntry, PackComponentKind, PackComponentListing, ResolvedPackComponent, } from './components.js';
|
|
4
6
|
export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
|
|
5
7
|
export type { ComposeDirectiveNameResult, NamespaceCollision, NamespaceValidationResult, } from './namespace.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// @markii/pack: the neutral component-pack contract (docs/packs.md). This
|
|
2
|
-
//
|
|
3
|
-
// hand-rolled validation only
|
|
4
|
-
// no sandboxed `require`, no filesystem reads
|
|
2
|
+
// started as slice 0 of issue #3 — the manifest shape, namespace/engine
|
|
3
|
+
// rules, and hand-rolled validation only, with no registry loading, no
|
|
4
|
+
// `uses:` surfacing, no sandboxed `require`, and no filesystem reads. Issue
|
|
5
|
+
// #16's `.mkp` archive reader (`./archive.ts`) is the first filesystem-
|
|
6
|
+
// adjacent slice: it reads validated bytes in and returns parsed contents
|
|
7
|
+
// out, touching no real filesystem itself (a host does that).
|
|
5
8
|
export { parsePackManifest } from './manifest.js';
|
|
9
|
+
export { openPackArchive, packArchiveFileName } from './archive.js';
|
|
6
10
|
export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
|
|
7
11
|
export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
|
|
8
12
|
export { isValidPackNameShape, resolveUses } from './uses.js';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -14,12 +14,31 @@ import type { PackComponentEntry } from './components.js';
|
|
|
14
14
|
* `kind`/`attributes` metadata (see `./components.ts`,
|
|
15
15
|
* `resolvePackComponent`, `packComponents`).
|
|
16
16
|
*
|
|
17
|
+
* `components` may be an EMPTY object. There is exactly one way to share
|
|
18
|
+
* Lua beyond a bundle's own `scripts/` folder: a pack module. A folder of
|
|
19
|
+
* shared Lua with nothing to render is simply a pack whose `components`
|
|
20
|
+
* is `{}` and which carries its own `scripts/*.lua` — it contributes zero
|
|
21
|
+
* directives and never appears in a component catalog, but its modules
|
|
22
|
+
* still resolve through `require "name/module"` like any other pack's.
|
|
23
|
+
*
|
|
17
24
|
* Shared Lua modules are NOT a manifest field. docs/packs.md says they
|
|
18
25
|
* travel under a conventional `scripts/` directory inside the pack
|
|
19
26
|
* (`require "ana/http"`), the same way a bundle's `scripts/` works — there
|
|
20
27
|
* is no separate manifest declaration for them, so this type doesn't add
|
|
21
|
-
* one.
|
|
22
|
-
*
|
|
28
|
+
* one.
|
|
29
|
+
*
|
|
30
|
+
* `version` is an OPTIONAL fourth field: a plain semver string
|
|
31
|
+
* (`MAJOR.MINOR.PATCH`, digits only, no leading zeros on a multi-digit
|
|
32
|
+
* component — see `SEMVER_PATTERN`). A pack that omits it keeps loading
|
|
33
|
+
* exactly as before, with no warning. Prerelease/build suffixes
|
|
34
|
+
* (`-beta.1`, `+build5`) are deliberately NOT accepted: AGENTS.md already
|
|
35
|
+
* commits this project to "plain semver" for spec versioning, and a pack
|
|
36
|
+
* version exists so a host can compare/display it, not to drive a resolver
|
|
37
|
+
* that needs prerelease ordering. Accepting a wider grammar than the one
|
|
38
|
+
* fact this field is for would be a casual-user-facing complication with
|
|
39
|
+
* no present consumer. A malformed `version` is an ERROR, not a
|
|
40
|
+
* forward-compatible warning, the same posture `name` gets: a version a
|
|
41
|
+
* host cannot trust is worse than no version.
|
|
23
42
|
*/
|
|
24
43
|
export interface PackManifest {
|
|
25
44
|
/** The pack's namespace, e.g. `"ana"`. See `validatePackName`. */
|
|
@@ -35,6 +54,12 @@ export interface PackManifest {
|
|
|
35
54
|
* the one place both forms are normalized.
|
|
36
55
|
*/
|
|
37
56
|
components: Record<string, PackComponentEntry>;
|
|
57
|
+
/**
|
|
58
|
+
* Optional plain-semver version string, e.g. `"1.0.0"`. Absent unless
|
|
59
|
+
* the pack author declared one. See this interface's doc comment for
|
|
60
|
+
* the accepted grammar and why prerelease/build suffixes are rejected.
|
|
61
|
+
*/
|
|
62
|
+
version?: string;
|
|
38
63
|
}
|
|
39
64
|
export type PackManifestParseResult = {
|
|
40
65
|
ok: true;
|
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { validateLocalComponentName, validatePackName } from './namespace.js';
|
|
2
2
|
import { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, } from './components.js';
|
|
3
|
-
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
3
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
4
|
+
'name',
|
|
5
|
+
'engine',
|
|
6
|
+
'components',
|
|
7
|
+
'version',
|
|
8
|
+
]);
|
|
9
|
+
// Plain semver only: MAJOR.MINOR.PATCH, digits only, no leading zeros on a
|
|
10
|
+
// multi-digit component ("1.0.0" and "0.0.1" are valid; "01.0.0" is not).
|
|
11
|
+
// No prerelease or build metadata suffix — see PackManifest's doc comment
|
|
12
|
+
// for why this field deliberately does not accept the wider semver grammar.
|
|
13
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
4
14
|
// The keys a component's object-form entry may carry, and which of those
|
|
5
15
|
// are required. Mirrors KNOWN_TOP_LEVEL_KEYS's role for the manifest root:
|
|
6
16
|
// an unrecognized key here is a forward-compatible warning, not an error.
|
|
@@ -324,14 +334,28 @@ export function parsePackManifest(json) {
|
|
|
324
334
|
sanitized.attributes = attributes;
|
|
325
335
|
result[key] = sanitized;
|
|
326
336
|
}
|
|
327
|
-
if (Object.keys(componentsObj).length === 0) {
|
|
328
|
-
errors.push('"components" must have at least one entry');
|
|
329
|
-
componentsValid = false;
|
|
330
|
-
}
|
|
331
337
|
if (componentsValid) {
|
|
332
338
|
components = result;
|
|
333
339
|
}
|
|
334
340
|
}
|
|
341
|
+
// --- version (optional) ---
|
|
342
|
+
//
|
|
343
|
+
// Absent is valid and produces neither an error nor a warning. Present
|
|
344
|
+
// but malformed is an ERROR (rejects the whole manifest), not a
|
|
345
|
+
// forward-compatible warning: a version a host cannot trust is worse
|
|
346
|
+
// than no version at all, the same posture as a malformed `name`.
|
|
347
|
+
let version;
|
|
348
|
+
let versionValid = true;
|
|
349
|
+
if (Object.hasOwn(obj, 'version')) {
|
|
350
|
+
const rawVersion = obj.version;
|
|
351
|
+
if (typeof rawVersion !== 'string' || !SEMVER_PATTERN.test(rawVersion)) {
|
|
352
|
+
errors.push('"version" must be a plain semver string ("MAJOR.MINOR.PATCH", digits only, no leading zeros, no prerelease or build suffix)');
|
|
353
|
+
versionValid = false;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
version = rawVersion;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
335
359
|
// --- unknown top-level keys: forward-compat warning, not an error ---
|
|
336
360
|
for (const key of Object.keys(obj)) {
|
|
337
361
|
if (Object.hasOwn(obj, key) && !KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
@@ -341,12 +365,15 @@ export function parsePackManifest(json) {
|
|
|
341
365
|
if (errors.length > 0 ||
|
|
342
366
|
name === undefined ||
|
|
343
367
|
engine === undefined ||
|
|
344
|
-
components === undefined
|
|
368
|
+
components === undefined ||
|
|
369
|
+
!versionValid) {
|
|
345
370
|
return { ok: false, errors };
|
|
346
371
|
}
|
|
347
372
|
return {
|
|
348
373
|
ok: true,
|
|
349
|
-
manifest:
|
|
374
|
+
manifest: version === undefined
|
|
375
|
+
? { name, engine, components }
|
|
376
|
+
: { name, engine, components, version },
|
|
350
377
|
warnings,
|
|
351
378
|
};
|
|
352
379
|
}
|
package/dist/namespace.d.ts
CHANGED
|
@@ -92,9 +92,10 @@ export interface NamespaceCollision {
|
|
|
92
92
|
/**
|
|
93
93
|
* Given several installed pack namespaces, returns the ones that appear
|
|
94
94
|
* more than once. Pure predicate only — docs/packs.md says installing two
|
|
95
|
-
* packs with the same namespace is rejected at install time
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* packs with the same namespace is rejected at install time; `@markii/host`'s
|
|
96
|
+
* `discoverPacks` (`packages/markii-host/src/packs/discover.ts`) is the
|
|
97
|
+
* caller that applies this at install time, excluding every pack sharing a
|
|
98
|
+
* colliding namespace.
|
|
98
99
|
*
|
|
99
100
|
* Comparison is exact-string (case-sensitive), matching `validatePackName`
|
|
100
101
|
* which already forces lowercase, so two namespaces differing only in case
|
package/dist/namespace.js
CHANGED
|
@@ -172,9 +172,10 @@ export function composeDirectiveName(packName, localName) {
|
|
|
172
172
|
/**
|
|
173
173
|
* Given several installed pack namespaces, returns the ones that appear
|
|
174
174
|
* more than once. Pure predicate only — docs/packs.md says installing two
|
|
175
|
-
* packs with the same namespace is rejected at install time
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* packs with the same namespace is rejected at install time; `@markii/host`'s
|
|
176
|
+
* `discoverPacks` (`packages/markii-host/src/packs/discover.ts`) is the
|
|
177
|
+
* caller that applies this at install time, excluding every pack sharing a
|
|
178
|
+
* colliding namespace.
|
|
178
179
|
*
|
|
179
180
|
* Comparison is exact-string (case-sensitive), matching `validatePackName`
|
|
180
181
|
* which already forces lowercase, so two namespaces differing only in case
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/pack",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Component pack contract for Markii (docs/packs.md): the pack.json manifest shape, namespace and engine rules,
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Component pack contract for Markii (docs/packs.md): the pack.json manifest shape, namespace and engine rules, hand-rolled validation, and the .mkp prebuilt-archive reader. No React, no parsing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
7
7
|
"mark",
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
"build": "tsc --noEmit -p tsconfig.json",
|
|
41
41
|
"build:dist": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
42
42
|
"lint": "eslint ."
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@markii/bundle": "0.12.0"
|
|
43
46
|
}
|
|
44
47
|
}
|