@markii/pack 0.15.0 → 0.17.0-next.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 CHANGED
@@ -41,6 +41,8 @@ export interface PackArchiveContents {
41
41
  readonly scriptBytes: Uint8Array;
42
42
  /** The prebuilt stylesheet's bytes (`webview.css`), present only when the pack ships one. */
43
43
  readonly stylesheetBytes?: Uint8Array;
44
+ /** The prebuilt terminal registration module (`terminal.js`), present only when the pack ships terminal views. Hosts that run no terminal engine ignore it. */
45
+ readonly terminalScriptBytes?: Uint8Array;
44
46
  /** 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
47
  readonly scriptModules: Readonly<Record<string, Uint8Array>>;
46
48
  /**
package/dist/archive.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `.mkp` pack archives (docs/packs.md, issue #16): a zip of a pack's
3
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
4
+ * `webview.css` when the pack has styles, `terminal.js` when it ships terminal views, `scripts/` when the pack ships
5
5
  * shared Lua. This is the reader half: given the raw bytes of a `.mkp`
6
6
  * file, validate it and expose its contents for read-only loading.
7
7
  *
@@ -37,6 +37,8 @@ import { parsePackManifest } from './manifest.js';
37
37
  const MANIFEST_ENTRY = 'pack.json';
38
38
  const SCRIPT_ENTRY = 'webview.js';
39
39
  const STYLESHEET_ENTRY = 'webview.css';
40
+ /** The optional terminal registration module (docs/packs.md). A pack with no terminal views has none. */
41
+ const TERMINAL_ENTRY = 'terminal.js';
40
42
  const SCRIPTS_DIR_PREFIX = 'scripts/';
41
43
  /** macOS's zip metadata folder — never a real pack file, and never counted toward "is this archive a single wrapped folder" detection below (batch 7 #61). */
42
44
  const MACOSX_PREFIX = '__MACOSX/';
@@ -198,12 +200,17 @@ export async function openPackArchive(bytes, options = {}) {
198
200
  if (paths.includes(STYLESHEET_ENTRY)) {
199
201
  stylesheetBytes = await storage.read(STYLESHEET_ENTRY);
200
202
  }
203
+ let terminalScriptBytes;
204
+ if (paths.includes(TERMINAL_ENTRY)) {
205
+ terminalScriptBytes = await storage.read(TERMINAL_ENTRY);
206
+ }
201
207
  const scriptModules = Object.create(null);
202
208
  const ignoredEntries = [];
203
209
  for (const path of paths) {
204
210
  if (path === MANIFEST_ENTRY ||
205
211
  path === SCRIPT_ENTRY ||
206
- path === STYLESHEET_ENTRY) {
212
+ path === STYLESHEET_ENTRY ||
213
+ path === TERMINAL_ENTRY) {
207
214
  continue;
208
215
  }
209
216
  if (isScriptModulePath(path)) {
@@ -222,6 +229,7 @@ export async function openPackArchive(bytes, options = {}) {
222
229
  manifestWarnings: parsed.warnings,
223
230
  scriptBytes,
224
231
  ...(stylesheetBytes !== undefined ? { stylesheetBytes } : {}),
232
+ ...(terminalScriptBytes !== undefined ? { terminalScriptBytes } : {}),
225
233
  scriptModules,
226
234
  ignoredEntries,
227
235
  },
@@ -71,7 +71,14 @@ export interface PackComponentAttribute {
71
71
  * hover documentation.
72
72
  */
73
73
  export interface PackComponentDefinition {
74
- source: string;
74
+ /** The source for the pack's top-level `engine`. Mutually exclusive with `sources`. */
75
+ source?: string;
76
+ /**
77
+ * Engine id -> pack-relative source path, for a component that ships
78
+ * more than one implementation (for example `react` and `ansi`).
79
+ * Mutually exclusive with `source`.
80
+ */
81
+ sources?: Readonly<Record<string, string>>;
75
82
  description?: string;
76
83
  kind?: PackComponentKind;
77
84
  /**
@@ -93,7 +100,13 @@ export type PackComponentEntry = string | PackComponentDefinition;
93
100
  * the manifest declared them (the string shorthand never has any of them).
94
101
  */
95
102
  export interface ResolvedPackComponent {
96
- readonly source: string;
103
+ /**
104
+ * Engine id -> pack-relative source path, normalized from all three
105
+ * declared forms (string shorthand and `source` are keyed under the
106
+ * manifest's `engine`). Read it through `componentSourceForEngine`.
107
+ * Never empty. A null-prototype object.
108
+ */
109
+ readonly sources: Readonly<Record<string, string>>;
97
110
  readonly description?: string;
98
111
  readonly kind?: PackComponentKind;
99
112
  readonly attributes?: readonly PackComponentAttribute[];
@@ -102,6 +115,38 @@ export interface ResolvedPackComponent {
102
115
  export interface PackComponentListing extends ResolvedPackComponent {
103
116
  readonly localName: string;
104
117
  }
118
+ /** A listing narrowed to one engine: `source` is that engine's source path. */
119
+ export interface EnginePackComponentListing extends PackComponentListing {
120
+ readonly source: string;
121
+ }
122
+ /**
123
+ * The pack-relative source this component has for `engine`, or `undefined`
124
+ * when it ships none. The one answer to "does this component run here".
125
+ */
126
+ export declare function componentSourceForEngine(component: Pick<ResolvedPackComponent, 'sources'>, engine: string): string | undefined;
127
+ /**
128
+ * Whether a host for `engine` should install the pack: true when the pack
129
+ * declares no components (Lua-only) or at least one component has a source
130
+ * for `engine`. A pack with components but none for this engine is not
131
+ * installable here and a host reports it as dropped.
132
+ */
133
+ export declare function packSupportsEngine(manifest: Pick<PackManifest, 'components' | 'engine'>, engine: string): boolean;
134
+ /**
135
+ * The pack's components that have a source for `engine`, in declaration
136
+ * order, each carrying that engine's `source`. Components without one are
137
+ * absent (they fall back to the unknown-component box in that engine).
138
+ */
139
+ export declare function packComponentsForEngine(manifest: Pick<PackManifest, 'components' | 'engine'>, engine: string): EnginePackComponentListing[];
140
+ /**
141
+ * Every engine id any component of the pack ships a source for, sorted,
142
+ * comma-joined for a "dropped pack" report. Falls back to the manifest's
143
+ * top-level `engine` when the pack declares no components.
144
+ */
145
+ export declare function packDeclaredEngines(manifest: Pick<PackManifest, 'components' | 'engine'>): string;
146
+ /** Engine ids are lowercase words: a leading letter, then lowercase letters, digits and hyphens. */
147
+ export declare const PACK_ENGINE_ID_PATTERN: RegExp;
148
+ /** Whether `id` is a usable engine id (a `sources` key). */
149
+ export declare function isPackEngineId(id: unknown): id is string;
105
150
  /**
106
151
  * Normalizes a single `components` entry, string or object form, into the
107
152
  * common `ResolvedPackComponent` shape. This is THE accessor for reading
@@ -116,8 +161,11 @@ export interface PackComponentListing extends ResolvedPackComponent {
116
161
  * a value. This matters because a consumer may call this directly on
117
162
  * attacker-controlled JSON that never went through `parsePackManifest` at
118
163
  * all (for example a pack loaded from an untrusted bundle).
164
+ *
165
+ * `defaultEngine` is the manifest's top-level `engine`: the engine the
166
+ * string shorthand and an object entry's `source` are written for.
119
167
  */
120
- export declare function resolvePackComponent(entry: unknown): ResolvedPackComponent | undefined;
168
+ export declare function resolvePackComponent(entry: unknown, defaultEngine: string): ResolvedPackComponent | undefined;
121
169
  /**
122
170
  * THE iterator every consumer uses instead of walking `manifest.components`
123
171
  * directly: `Object.hasOwn`-guarded, in declaration key order, skipping any
@@ -131,4 +179,4 @@ export declare function resolvePackComponent(entry: unknown): ResolvedPackCompon
131
179
  * object, or an array: all of those return `[]` rather than throwing, so a
132
180
  * caller never needs to guard the call site itself.
133
181
  */
134
- export declare function packComponents(manifest: Pick<PackManifest, 'components'>): PackComponentListing[];
182
+ export declare function packComponents(manifest: Pick<PackManifest, 'components' | 'engine'>): PackComponentListing[];
@@ -29,6 +29,92 @@ export const PACK_ATTRIBUTE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
29
29
  export function isPackAttributeName(name) {
30
30
  return typeof name === 'string' && PACK_ATTRIBUTE_NAME_PATTERN.test(name);
31
31
  }
32
+ /**
33
+ * The pack-relative source this component has for `engine`, or `undefined`
34
+ * when it ships none. The one answer to "does this component run here".
35
+ */
36
+ export function componentSourceForEngine(component, engine) {
37
+ return Object.hasOwn(component.sources, engine)
38
+ ? component.sources[engine]
39
+ : undefined;
40
+ }
41
+ /**
42
+ * Whether a host for `engine` should install the pack: true when the pack
43
+ * declares no components (Lua-only) or at least one component has a source
44
+ * for `engine`. A pack with components but none for this engine is not
45
+ * installable here and a host reports it as dropped.
46
+ */
47
+ export function packSupportsEngine(manifest, engine) {
48
+ const all = packComponents(manifest);
49
+ if (all.length === 0)
50
+ return true;
51
+ return all.some((c) => componentSourceForEngine(c, engine) !== undefined);
52
+ }
53
+ /**
54
+ * The pack's components that have a source for `engine`, in declaration
55
+ * order, each carrying that engine's `source`. Components without one are
56
+ * absent (they fall back to the unknown-component box in that engine).
57
+ */
58
+ export function packComponentsForEngine(manifest, engine) {
59
+ const result = [];
60
+ for (const listing of packComponents(manifest)) {
61
+ const source = componentSourceForEngine(listing, engine);
62
+ if (source !== undefined)
63
+ result.push({ ...listing, source });
64
+ }
65
+ return result;
66
+ }
67
+ /**
68
+ * Every engine id any component of the pack ships a source for, sorted,
69
+ * comma-joined for a "dropped pack" report. Falls back to the manifest's
70
+ * top-level `engine` when the pack declares no components.
71
+ */
72
+ export function packDeclaredEngines(manifest) {
73
+ const ids = new Set();
74
+ for (const listing of packComponents(manifest)) {
75
+ for (const id of Object.keys(listing.sources))
76
+ ids.add(id);
77
+ }
78
+ if (ids.size === 0)
79
+ ids.add(manifest.engine);
80
+ return [...ids].sort().join(', ');
81
+ }
82
+ /** Engine ids are lowercase words: a leading letter, then lowercase letters, digits and hyphens. */
83
+ export const PACK_ENGINE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
84
+ /** Whether `id` is a usable engine id (a `sources` key). */
85
+ export function isPackEngineId(id) {
86
+ return typeof id === 'string' && PACK_ENGINE_ID_PATTERN.test(id);
87
+ }
88
+ function nullProto(entries) {
89
+ const out = Object.create(null);
90
+ for (const [k, v] of entries)
91
+ out[k] = v;
92
+ return out;
93
+ }
94
+ /** Reads the engine-to-source map of an object entry, leniently: bad pairs are dropped. */
95
+ function resolveSources(entry, defaultEngine) {
96
+ const source = ownProperty(entry, 'source');
97
+ const sources = ownProperty(entry, 'sources');
98
+ if (typeof source === 'string' && source.length > 0) {
99
+ if (sources !== undefined)
100
+ return undefined;
101
+ return nullProto([[defaultEngine, source]]);
102
+ }
103
+ if (sources === null ||
104
+ typeof sources !== 'object' ||
105
+ Array.isArray(sources)) {
106
+ return undefined;
107
+ }
108
+ const pairs = [];
109
+ for (const key of Object.keys(sources)) {
110
+ if (!Object.hasOwn(sources, key) || !isPackEngineId(key))
111
+ continue;
112
+ const value = sources[key];
113
+ if (typeof value === 'string' && value.length > 0)
114
+ pairs.push([key, value]);
115
+ }
116
+ return pairs.length > 0 ? nullProto(pairs) : undefined;
117
+ }
32
118
  /**
33
119
  * Reads one own property off an already-narrowed object, or `undefined`
34
120
  * when it is absent. `Object.hasOwn` only, matching this package's rule
@@ -112,20 +198,24 @@ function resolveAttributes(raw) {
112
198
  * a value. This matters because a consumer may call this directly on
113
199
  * attacker-controlled JSON that never went through `parsePackManifest` at
114
200
  * all (for example a pack loaded from an untrusted bundle).
201
+ *
202
+ * `defaultEngine` is the manifest's top-level `engine`: the engine the
203
+ * string shorthand and an object entry's `source` are written for.
115
204
  */
116
- export function resolvePackComponent(entry) {
205
+ export function resolvePackComponent(entry, defaultEngine) {
117
206
  if (typeof entry === 'string') {
118
- return entry.length > 0 ? { source: entry } : undefined;
207
+ return entry.length > 0
208
+ ? { sources: nullProto([[defaultEngine, entry]]) }
209
+ : undefined;
119
210
  }
120
211
  if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
121
212
  return undefined;
122
213
  }
123
- const source = ownProperty(entry, 'source');
124
- if (typeof source !== 'string' || source.length === 0) {
214
+ const sources = resolveSources(entry, defaultEngine);
215
+ if (sources === undefined)
125
216
  return undefined;
126
- }
127
217
  const result = {
128
- source,
218
+ sources,
129
219
  };
130
220
  const description = ownProperty(entry, 'description');
131
221
  if (typeof description === 'string' && description.length > 0) {
@@ -156,6 +246,7 @@ export function resolvePackComponent(entry) {
156
246
  */
157
247
  export function packComponents(manifest) {
158
248
  const components = manifest?.components;
249
+ const engine = manifest?.engine;
159
250
  if (components === null ||
160
251
  typeof components !== 'object' ||
161
252
  Array.isArray(components)) {
@@ -165,7 +256,7 @@ export function packComponents(manifest) {
165
256
  for (const localName of Object.keys(components)) {
166
257
  if (!Object.hasOwn(components, localName))
167
258
  continue;
168
- const resolved = resolvePackComponent(components[localName]);
259
+ const resolved = resolvePackComponent(components[localName], typeof engine === 'string' ? engine : '');
169
260
  if (resolved === undefined)
170
261
  continue;
171
262
  listings.push({ localName, ...resolved });
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export type { PackManifest, PackManifestParseResult } from './manifest.js';
2
2
  export { parsePackManifest } from './manifest.js';
3
3
  export type { OpenPackArchiveOptions, OpenPackArchiveResult, PackArchiveContents, PackArchiveError, } from './archive.js';
4
4
  export { openPackArchive, packArchiveFileName } from './archive.js';
5
- export type { PackComponentAttribute, PackComponentDefinition, PackComponentEntry, PackComponentKind, PackComponentListing, ResolvedPackComponent, } from './components.js';
6
- export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
5
+ export type { PackComponentAttribute, PackComponentDefinition, PackComponentEntry, PackComponentKind, EnginePackComponentListing, PackComponentListing, ResolvedPackComponent, } from './components.js';
6
+ export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, PACK_ENGINE_ID_PATTERN, componentSourceForEngine, isPackAttributeName, isPackEngineId, packComponents, packComponentsForEngine, packDeclaredEngines, packSupportsEngine, resolvePackComponent, } from './components.js';
7
7
  export type { ComposeDirectiveNameResult, NamespaceCollision, NamespaceValidationResult, } from './namespace.js';
8
8
  export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
9
9
  export type { UsesResolution } from './uses.js';
package/dist/index.js CHANGED
@@ -7,6 +7,6 @@
7
7
  // out, touching no real filesystem itself (a host does that).
8
8
  export { parsePackManifest } from './manifest.js';
9
9
  export { openPackArchive, packArchiveFileName } from './archive.js';
10
- export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
10
+ export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, PACK_ENGINE_ID_PATTERN, componentSourceForEngine, isPackAttributeName, isPackEngineId, packComponents, packComponentsForEngine, packDeclaredEngines, packSupportsEngine, resolvePackComponent, } from './components.js';
11
11
  export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
12
12
  export { isValidPackNameShape, resolveUses } from './uses.js';
package/dist/manifest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { validateLocalComponentName, validatePackName } from './namespace.js';
2
- import { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, } from './components.js';
2
+ import { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, PACK_ENGINE_ID_PATTERN, isPackAttributeName, isPackEngineId, } from './components.js';
3
3
  const KNOWN_TOP_LEVEL_KEYS = new Set([
4
4
  'name',
5
5
  'engine',
@@ -16,6 +16,7 @@ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
16
16
  // an unrecognized key here is a forward-compatible warning, not an error.
17
17
  const KNOWN_COMPONENT_KEYS = new Set([
18
18
  'source',
19
+ 'sources',
19
20
  'description',
20
21
  'kind',
21
22
  'attributes',
@@ -266,11 +267,57 @@ export function parsePackManifest(json) {
266
267
  // the raw parsed one — the same discipline as the string-form branch
267
268
  // and as the top-level Object.hasOwn guarding throughout this file.
268
269
  let entryValid = true;
269
- const source = Object.hasOwn(value, 'source') ? value.source : undefined;
270
- if (typeof source !== 'string' || source.length === 0) {
271
- errors.push(`"components.${key}.source" must be a non-empty string (a pack-relative source path)`);
270
+ const hasSource = Object.hasOwn(value, 'source');
271
+ const hasSources = Object.hasOwn(value, 'sources');
272
+ const source = hasSource ? value.source : undefined;
273
+ let sourcesMap;
274
+ if (hasSource && hasSources) {
275
+ errors.push(`"components.${key}" must carry "source" or "sources", not both`);
272
276
  entryValid = false;
273
277
  }
278
+ else if (!hasSource && !hasSources) {
279
+ errors.push(`"components.${key}.source" is required (a pack-relative source path), or give a "sources" map instead`);
280
+ entryValid = false;
281
+ }
282
+ else if (hasSource) {
283
+ if (typeof source !== 'string' || source.length === 0) {
284
+ errors.push(`"components.${key}.source" must be a non-empty string (a pack-relative source path)`);
285
+ entryValid = false;
286
+ }
287
+ }
288
+ else {
289
+ const rawSources = value.sources;
290
+ if (!isPlainObject(rawSources)) {
291
+ errors.push(`"components.${key}.sources" must be an object mapping engine ids to pack-relative source paths`);
292
+ entryValid = false;
293
+ }
294
+ else {
295
+ const parsedSources = Object.create(null);
296
+ let count = 0;
297
+ for (const engineId of Object.keys(rawSources)) {
298
+ if (!Object.hasOwn(rawSources, engineId))
299
+ continue;
300
+ count += 1;
301
+ const path = rawSources[engineId];
302
+ if (!isPackEngineId(engineId)) {
303
+ errors.push(`"components.${key}.sources" key "${engineId}" must be a lowercase engine id matching ${PACK_ENGINE_ID_PATTERN.source}`);
304
+ entryValid = false;
305
+ }
306
+ else if (typeof path !== 'string' || path.length === 0) {
307
+ errors.push(`"components.${key}.sources.${engineId}" must be a non-empty string (a pack-relative source path)`);
308
+ entryValid = false;
309
+ }
310
+ else {
311
+ parsedSources[engineId] = path;
312
+ }
313
+ }
314
+ if (count === 0) {
315
+ errors.push(`"components.${key}.sources" must name at least one engine`);
316
+ entryValid = false;
317
+ }
318
+ sourcesMap = parsedSources;
319
+ }
320
+ }
274
321
  let description;
275
322
  if (Object.hasOwn(value, 'description')) {
276
323
  const rawDescription = value.description;
@@ -325,7 +372,7 @@ export function parsePackManifest(json) {
325
372
  // field was present — only the string shorthand collapses to a bare
326
373
  // string. This preserves the author's declared form rather than
327
374
  // silently normalizing `{ "source": "./x.tsx" }` into `"./x.tsx"`.
328
- const sanitized = { source: source };
375
+ const sanitized = hasSource ? { source: source } : { sources: sourcesMap };
329
376
  if (description !== undefined)
330
377
  sanitized.description = description;
331
378
  if (kind !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/pack",
3
- "version": "0.15.0",
3
+ "version": "0.17.0-next.0",
4
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",
@@ -42,6 +42,6 @@
42
42
  "lint": "eslint ."
43
43
  },
44
44
  "dependencies": {
45
- "@markii/bundle": "^0.15.0"
45
+ "@markii/bundle": "^0.17.0-next.0"
46
46
  }
47
47
  }