@hyperfrontend/features 0.1.0 → 0.2.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.
Files changed (159) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/_dependencies/@hyperfrontend/builder/bundle/dependencies/index.cjs.js +1 -0
  3. package/_dependencies/@hyperfrontend/builder/bundle/dependencies/index.esm.js +1 -0
  4. package/_dependencies/@hyperfrontend/builder/bundle/dependencies/worker/index.cjs.js +1 -0
  5. package/_dependencies/@hyperfrontend/builder/bundle/dependencies/worker/index.esm.js +1 -0
  6. package/_dependencies/@hyperfrontend/builder/bundle/index.cjs.js +12 -10
  7. package/_dependencies/@hyperfrontend/builder/bundle/index.esm.js +14 -12
  8. package/_dependencies/@hyperfrontend/builder/bundle/rollup/index.cjs.js +2 -0
  9. package/_dependencies/@hyperfrontend/builder/bundle/rollup/index.esm.js +2 -0
  10. package/_dependencies/@hyperfrontend/builder/bundle/rollup/worker/index.cjs.js +2 -0
  11. package/_dependencies/@hyperfrontend/builder/bundle/rollup/worker/index.esm.js +2 -0
  12. package/_dependencies/@hyperfrontend/builder/index.cjs.js +87 -53
  13. package/_dependencies/@hyperfrontend/builder/index.esm.js +89 -55
  14. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.cjs.js +4 -0
  15. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.esm.js +3 -1
  16. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/reflect/index.cjs.js +10 -0
  17. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/reflect/index.esm.js +6 -0
  18. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/timers/index.cjs.js +5 -0
  19. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/timers/index.esm.js +5 -1
  20. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/typed-arrays/index.cjs.js +2 -2
  21. package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/typed-arrays/index.esm.js +2 -2
  22. package/_dependencies/@hyperfrontend/network-protocol/browser/channel/index.cjs.js +5 -19
  23. package/_dependencies/@hyperfrontend/network-protocol/browser/channel/index.esm.js +1 -15
  24. package/_dependencies/@hyperfrontend/network-protocol/browser/data/index.cjs.js +15 -23
  25. package/_dependencies/@hyperfrontend/network-protocol/browser/data/index.esm.js +7 -15
  26. package/_dependencies/@hyperfrontend/network-protocol/browser/packet/index.cjs.js +6 -14
  27. package/_dependencies/@hyperfrontend/network-protocol/browser/packet/index.esm.js +7 -15
  28. package/_dependencies/@hyperfrontend/network-protocol/browser/receiver/index.cjs.js +4 -18
  29. package/_dependencies/@hyperfrontend/network-protocol/browser/receiver/index.esm.js +1 -15
  30. package/_dependencies/@hyperfrontend/network-protocol/browser/sender/index.cjs.js +5 -19
  31. package/_dependencies/@hyperfrontend/network-protocol/browser/sender/index.esm.js +2 -16
  32. package/_dependencies/@hyperfrontend/network-protocol/browser/v1/index.cjs.js +16 -24
  33. package/_dependencies/@hyperfrontend/network-protocol/browser/v1/index.esm.js +7 -15
  34. package/_dependencies/@hyperfrontend/network-protocol/browser/v2/index.cjs.js +16 -24
  35. package/_dependencies/@hyperfrontend/network-protocol/browser/v2/index.esm.js +7 -15
  36. package/_dependencies/@hyperfrontend/network-protocol/node/channel/index.cjs.js +3 -17
  37. package/_dependencies/@hyperfrontend/network-protocol/node/channel/index.esm.js +1 -15
  38. package/_dependencies/@hyperfrontend/network-protocol/node/data/index.cjs.js +6 -14
  39. package/_dependencies/@hyperfrontend/network-protocol/node/data/index.esm.js +7 -15
  40. package/_dependencies/@hyperfrontend/network-protocol/node/packet/index.cjs.js +6 -14
  41. package/_dependencies/@hyperfrontend/network-protocol/node/packet/index.esm.js +7 -15
  42. package/_dependencies/@hyperfrontend/network-protocol/node/receiver/index.cjs.js +3 -17
  43. package/_dependencies/@hyperfrontend/network-protocol/node/receiver/index.esm.js +1 -15
  44. package/_dependencies/@hyperfrontend/network-protocol/node/sender/index.cjs.js +2 -16
  45. package/_dependencies/@hyperfrontend/network-protocol/node/sender/index.esm.js +2 -16
  46. package/_dependencies/@hyperfrontend/network-protocol/node/v1/index.cjs.js +6 -14
  47. package/_dependencies/@hyperfrontend/network-protocol/node/v1/index.esm.js +7 -15
  48. package/_dependencies/@hyperfrontend/network-protocol/node/v2/index.cjs.js +6 -14
  49. package/_dependencies/@hyperfrontend/network-protocol/node/v2/index.esm.js +7 -15
  50. package/_dependencies/@hyperfrontend/nexus/index.cjs.js +49 -19
  51. package/_dependencies/@hyperfrontend/nexus/index.esm.js +49 -19
  52. package/_dependencies/@hyperfrontend/project-scope/core/fs/index.cjs.js +62 -0
  53. package/_dependencies/@hyperfrontend/project-scope/core/fs/index.esm.js +60 -2
  54. package/_shared/generators/feature/generate-feature-module/index.esm.js +11 -6
  55. package/_shared/generators/metadata/generate-metadata/index.esm.js +1 -0
  56. package/_shared/shared/control/index.cjs.js +12 -2
  57. package/_shared/shared/control/index.esm.js +12 -2
  58. package/_shared/shared/request/index.cjs.js +91 -0
  59. package/_shared/shared/request/index.esm.js +88 -0
  60. package/_shared/shared/shutdown/index.esm.js +12 -0
  61. package/bin/hf.js +643 -70
  62. package/bundle/host/index.iife.js +290 -4041
  63. package/bundle/host/index.iife.min.js +1 -1
  64. package/bundle/host/index.umd.js +290 -4041
  65. package/bundle/host/index.umd.min.js +1 -1
  66. package/bundle/hostee/index.iife.js +215 -2893
  67. package/bundle/hostee/index.iife.min.js +1 -1
  68. package/bundle/hostee/index.umd.js +215 -2893
  69. package/bundle/hostee/index.umd.min.js +1 -1
  70. package/cli/args.d.ts +2 -0
  71. package/cli/args.d.ts.map +1 -1
  72. package/cli/commands/build.d.ts +8 -5
  73. package/cli/commands/build.d.ts.map +1 -1
  74. package/cli/commands/dev.d.ts +7 -2
  75. package/cli/commands/dev.d.ts.map +1 -1
  76. package/cli/config/resolve.d.ts +3 -1
  77. package/cli/config/resolve.d.ts.map +1 -1
  78. package/cli/index.cjs.js +643 -70
  79. package/cli/index.d.ts +21 -10
  80. package/cli/index.esm.js +591 -60
  81. package/cli/usage.d.ts +1 -1
  82. package/cli/usage.d.ts.map +1 -1
  83. package/generators/feature/generate-feature-module.d.ts.map +1 -1
  84. package/generators/index.cjs.js +435 -42
  85. package/generators/index.d.ts +9 -8
  86. package/generators/index.esm.js +404 -30
  87. package/generators/metadata/generate-metadata.d.ts +4 -4
  88. package/generators/metadata/generate-metadata.d.ts.map +1 -1
  89. package/generators/shell/connector-types.d.ts +19 -0
  90. package/generators/shell/connector-types.d.ts.map +1 -0
  91. package/generators/shell/generate-shell.d.ts +5 -4
  92. package/generators/shell/generate-shell.d.ts.map +1 -1
  93. package/generators/shell/schema-type.d.ts +20 -0
  94. package/generators/shell/schema-type.d.ts.map +1 -0
  95. package/generators/shell/source-literal.d.ts +28 -0
  96. package/generators/shell/source-literal.d.ts.map +1 -1
  97. package/host/create-shell.d.ts +4 -1
  98. package/host/create-shell.d.ts.map +1 -1
  99. package/host/display-modes/dialog.d.ts +1 -1
  100. package/host/display-modes/dialog.d.ts.map +1 -1
  101. package/host/display-modes/embedded.d.ts +1 -1
  102. package/host/display-modes/embedded.d.ts.map +1 -1
  103. package/host/index.cjs.js +150 -30
  104. package/host/index.d.ts +53 -38
  105. package/host/index.d.ts.map +1 -1
  106. package/host/index.esm.js +129 -9
  107. package/host/lifecycle.d.ts.map +1 -1
  108. package/host/plugins.d.ts +1 -34
  109. package/host/plugins.d.ts.map +1 -1
  110. package/host/types.d.ts +49 -0
  111. package/host/types.d.ts.map +1 -1
  112. package/hostee/index.cjs.js +54 -9
  113. package/hostee/index.d.ts +41 -1
  114. package/hostee/index.d.ts.map +1 -1
  115. package/hostee/index.esm.js +51 -6
  116. package/hostee/lifecycle.d.ts.map +1 -1
  117. package/hostee/types.d.ts +40 -0
  118. package/hostee/types.d.ts.map +1 -1
  119. package/index.cjs.js +32 -1
  120. package/index.d.ts +89 -3
  121. package/index.d.ts.map +1 -1
  122. package/index.esm.js +32 -1
  123. package/nx/executors/build/index.cjs.js +14975 -137
  124. package/nx/executors/build/index.esm.js +14935 -115
  125. package/nx/executors/serve/executor.d.ts.map +1 -1
  126. package/nx/executors/serve/index.cjs.js +6594 -80
  127. package/nx/executors/serve/index.esm.js +6529 -44
  128. package/nx/generators/feature/index.cjs.js +8751 -108
  129. package/nx/generators/feature/index.esm.js +8711 -81
  130. package/package.json +15 -5
  131. package/server/debug-ui/index.d.ts +2 -0
  132. package/server/debug-ui/index.d.ts.map +1 -0
  133. package/server/debug-ui/index.html +15 -0
  134. package/server/debug-ui/index.iife.js +427 -0
  135. package/server/debug-ui/index.iife.min.js +1 -0
  136. package/server/dev-server.d.ts.map +1 -1
  137. package/server/index.cjs.js +78 -10
  138. package/server/index.esm.js +78 -11
  139. package/server/module-dir.d.ts +17 -0
  140. package/server/module-dir.d.ts.map +1 -0
  141. package/server/module-dir.stub.d.ts +15 -0
  142. package/server/module-dir.stub.d.ts.map +1 -0
  143. package/shared/contract.d.ts +1 -1
  144. package/shared/contract.d.ts.map +1 -1
  145. package/shared/control.d.ts +4 -0
  146. package/shared/control.d.ts.map +1 -1
  147. package/shared/invert-contract.d.ts +20 -0
  148. package/shared/invert-contract.d.ts.map +1 -0
  149. package/shared/request.d.ts +68 -0
  150. package/shared/request.d.ts.map +1 -0
  151. package/{nx/shared → shared}/shutdown.d.ts +3 -2
  152. package/shared/shutdown.d.ts.map +1 -0
  153. package/shared/types.d.ts +72 -1
  154. package/shared/types.d.ts.map +1 -1
  155. package/_shared/nx/shared/context/index.cjs.js +0 -18
  156. package/_shared/nx/shared/context/index.esm.js +0 -16
  157. package/nx/shared/shutdown.d.ts.map +0 -1
  158. package/server/debug-ui/bootstrap.d.ts +0 -2
  159. package/server/debug-ui/bootstrap.d.ts.map +0 -1
package/bin/hf.js CHANGED
@@ -3,15 +3,14 @@
3
3
 
4
4
  const index_cjs_js = require('../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/error/index.cjs.js');
5
5
  const node_child_process = require('node:child_process');
6
- const node_os = require('node:os');
7
6
  const node_path = require('node:path');
8
7
  const index_cjs_js$8 = require('../_dependencies/@hyperfrontend/builder/index.cjs.js');
8
+ const index_cjs_js$4 = require('../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.cjs.js');
9
9
  const index_cjs_js$3 = require('../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/json/index.cjs.js');
10
10
  const index_cjs_js$6 = require('../_dependencies/@hyperfrontend/project-scope/core/fs/index.cjs.js');
11
11
  const index_cjs_js$7 = require('../_dependencies/@hyperfrontend/project-scope/vfs/index.cjs.js');
12
12
  const index_cjs_js$1 = require('../_dependencies/@hyperfrontend/versioning/semver/format/index.cjs.js');
13
13
  const index_cjs_js$2 = require('../_dependencies/@hyperfrontend/versioning/semver/parse/index.cjs.js');
14
- const index_cjs_js$4 = require('../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.cjs.js');
15
14
  const index_cjs_js$5 = require('../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js');
16
15
  require('../_dependencies/@hyperfrontend/json-utils/index.cjs.js');
17
16
  const node_url = require('node:url');
@@ -21,6 +20,7 @@ const index_cjs_js$a = require('../_dependencies/@hyperfrontend/immutable-api-ut
21
20
  const index_cjs_js$c = require('../_dependencies/@hyperfrontend/project-scope/heuristics/entry-points/index.cjs.js');
22
21
  const index_cjs_js$b = require('../_dependencies/@hyperfrontend/questions/index.cjs.js');
23
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
24
24
  /** Value-taking flags mapped to their {@link CliFlags} key. */
25
25
  const STRING_FLAGS = {
26
26
  '--name': 'name',
@@ -37,6 +37,7 @@ const STRING_FLAGS = {
37
37
  };
38
38
  /** Boolean flags mapped to their {@link CliFlags} key. */
39
39
  const BOOLEAN_FLAGS = {
40
+ '--allow-open': 'allowOpen',
40
41
  '--ci': 'ci',
41
42
  '--yes': 'yes',
42
43
  '--dry-run': 'dryRun',
@@ -86,6 +87,7 @@ function parseCliArgs(argv) {
86
87
  }
87
88
  const flags = {
88
89
  ...strings,
90
+ allowOpen: booleans['allowOpen'] ?? false,
89
91
  ci: booleans['ci'] ?? false,
90
92
  yes: booleans['yes'] ?? false,
91
93
  dryRun: booleans['dryRun'] ?? false,
@@ -100,16 +102,16 @@ const METADATA_PATH = 'metadata.json';
100
102
  * Stages the connector's `metadata.json` describing the feature and its contract.
101
103
  *
102
104
  * Stamps a canonical version string via `@hyperfrontend/versioning` and embeds
103
- * the contract so humans and the registry can inspect the feature without
104
- * unpacking the bundle.
105
+ * the contract and the baked security protocol so humans and the registry can
106
+ * inspect the feature without unpacking the bundle.
105
107
  *
106
- * @param config - The resolved feature config supplying name, version, and URL.
108
+ * @param config - The resolved feature config supplying name, version, URL, and protocol.
107
109
  * @param contract - The validated contract embedded for inspection.
108
110
  * @param tree - The VFS tree the metadata file is staged into.
109
111
  *
110
112
  * @example Staging metadata for the clock feature
111
113
  * ```typescript
112
- * generateMetadata({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock' }, contract, tree)
114
+ * generateMetadata({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock', protocol: 'v2' }, contract, tree)
113
115
  * ```
114
116
  */
115
117
  function generateMetadata(config, contract, tree) {
@@ -118,6 +120,7 @@ function generateMetadata(config, contract, tree) {
118
120
  version: index_cjs_js$1.format(index_cjs_js$2.parseVersionStrict(config.version)),
119
121
  url: config.url,
120
122
  contract,
123
+ ...(config.protocol !== undefined && { protocol: config.protocol }),
121
124
  generatedBy: '@hyperfrontend/features',
122
125
  };
123
126
  tree.write(METADATA_PATH, `${index_cjs_js$3.stringify(metadata, null, 2)}\n`);
@@ -166,6 +169,11 @@ function isIdentifier(key) {
166
169
  *
167
170
  * @param value - The string to render.
168
171
  * @returns A single-quoted, safely escaped string literal.
172
+ *
173
+ * @example Quoting a string containing a single quote
174
+ * ```typescript
175
+ * quoteString("it's") // => "'it\\'s'"
176
+ * ```
169
177
  */
170
178
  function quoteString(value) {
171
179
  // how: JSON encoding already escapes backslashes and control chars; we scan it to swap quote conventions (unescape \" to ", escape ' to \') while copying every other escape verbatim, so an input backslash stays intact.
@@ -192,6 +200,12 @@ function quoteString(value) {
192
200
  *
193
201
  * @param key - The property name.
194
202
  * @returns The key as written in an object literal.
203
+ *
204
+ * @example Formatting identifier and non-identifier keys
205
+ * ```typescript
206
+ * formatKey('url') // => 'url'
207
+ * formatKey('time-zone') // => "'time-zone'"
208
+ * ```
195
209
  */
196
210
  function formatKey(key) {
197
211
  return isIdentifier(key) ? key : quoteString(key);
@@ -239,43 +253,357 @@ function toSourceLiteral(value, indent = '') {
239
253
  return index_cjs_js$3.stringify(value);
240
254
  }
241
255
 
256
+ // note: JSON-schema scalar types mapped straight to their TypeScript equivalents; `integer` narrows to `number`.
257
+ const PRIMITIVES = {
258
+ string: 'string',
259
+ number: 'number',
260
+ integer: 'number',
261
+ boolean: 'boolean',
262
+ null: 'null',
263
+ };
264
+ /**
265
+ * Narrows an unknown value to a plain record.
266
+ *
267
+ * @param value - The value to test.
268
+ * @returns `true` when the value is a non-null, non-array object.
269
+ */
270
+ function isRecord$3(value) {
271
+ return typeof value === 'object' && value !== null && !index_cjs_js$4.isArray(value);
272
+ }
273
+ /**
274
+ * Renders a JSON scalar as a TypeScript literal type.
275
+ *
276
+ * @param value - The candidate literal.
277
+ * @returns The literal type source, or `null` for a non-scalar value.
278
+ */
279
+ function literalType(value) {
280
+ if (value === null) {
281
+ return 'null';
282
+ }
283
+ if (typeof value === 'string') {
284
+ return quoteString(value);
285
+ }
286
+ if (typeof value === 'number' || typeof value === 'boolean') {
287
+ return index_cjs_js$3.stringify(value);
288
+ }
289
+ return null;
290
+ }
291
+ /**
292
+ * Renders an `enum` member list as a union of literal types.
293
+ *
294
+ * @param members - The allowed values listed by the schema.
295
+ * @returns The union source, or `unknown` when any member is not a scalar.
296
+ */
297
+ function enumType(members) {
298
+ const literals = members.map(literalType);
299
+ // why: One non-scalar member would poison the union with `unknown`, which silently absorbs the useful literals — better to fall back for the whole enum.
300
+ return literals.every((literal) => literal !== null) ? literals.join(' | ') : 'unknown';
301
+ }
302
+ /**
303
+ * Renders an `object` schema as an inline type literal.
304
+ *
305
+ * @param schema - The schema record with optional `properties` and `required`.
306
+ * @param indent - The current indentation prefix.
307
+ * @returns The object type source; `Record<string, unknown>` without properties.
308
+ */
309
+ function objectType(schema, indent) {
310
+ const properties = isRecord$3(schema['properties']) ? index_cjs_js$5.entries(schema['properties']) : [];
311
+ if (properties.length === 0) {
312
+ return 'Record<string, unknown>';
313
+ }
314
+ const required = index_cjs_js$4.isArray(schema['required']) ? schema['required'] : [];
315
+ const inner = `${indent} `;
316
+ const members = properties.map(([key, member]) => `${inner}${formatKey(key)}${required.includes(key) ? '' : '?'}: ${schemaToType(member, inner)}`);
317
+ return `{\n${members.join('\n')}\n${indent}}`;
318
+ }
319
+ /**
320
+ * Renders an `array` schema as an element-type array.
321
+ *
322
+ * @param schema - The schema record with an optional single `items` schema.
323
+ * @param indent - The current indentation prefix.
324
+ * @returns The array type source; `unknown[]` without a single `items` schema.
325
+ */
326
+ function arrayType(schema, indent) {
327
+ const items = schema['items'];
328
+ if (!isRecord$3(items)) {
329
+ return 'unknown[]';
330
+ }
331
+ const element = schemaToType(items, indent);
332
+ return element.includes('|') ? `(${element})[]` : `${element}[]`;
333
+ }
334
+ /**
335
+ * Projects a JSON-schema-like payload description into TypeScript type source.
336
+ *
337
+ * Bounded mapping: scalar `type`s, `object` with `properties`/`required`,
338
+ * `array` with a single `items` schema, `enum`, and `const` are projected;
339
+ * anything else (including a missing schema) falls back to `unknown`, so a
340
+ * generated type is never wrong — at worst it is loose.
341
+ *
342
+ * @param schema - The schema value to project.
343
+ * @param indent - Indentation prefix applied to nested object members.
344
+ * @returns The TypeScript type as source text.
345
+ *
346
+ * @example Projecting an action payload schema
347
+ * ```typescript
348
+ * schemaToType({ type: 'object', properties: { tz: { type: 'string' } }, required: ['tz'] })
349
+ * // => '{\n tz: string\n}'
350
+ * ```
351
+ */
352
+ function schemaToType(schema, indent = '') {
353
+ if (!isRecord$3(schema)) {
354
+ return 'unknown';
355
+ }
356
+ const enumMembers = schema['enum'];
357
+ if (index_cjs_js$4.isArray(enumMembers) && enumMembers.length > 0) {
358
+ return enumType(enumMembers);
359
+ }
360
+ if ('const' in schema) {
361
+ return literalType(schema['const']) ?? 'unknown';
362
+ }
363
+ const type = schema['type'];
364
+ if (typeof type !== 'string') {
365
+ return 'unknown';
366
+ }
367
+ const primitive = PRIMITIVES[type];
368
+ if (primitive !== undefined) {
369
+ return primitive;
370
+ }
371
+ if (type === 'object') {
372
+ return objectType(schema, indent);
373
+ }
374
+ if (type === 'array') {
375
+ return arrayType(schema, indent);
376
+ }
377
+ return 'unknown';
378
+ }
379
+
380
+ // note: The generated declarations are structural (no type imports from the SDK), so the connector's d.ts stays fully usable for consumers who install nothing but the packed tarball.
381
+ const STATIC_TYPES = `/** How the feature is surfaced by the host. */
382
+ export type FeatureDisplayMode = 'embedded' | 'dialog' | 'popup' | 'standalone'
383
+
384
+ /** Security envelope selector negotiated between host and feature. */
385
+ export type FeatureSecurityProtocol = 'none' | 'v1' | 'v2'
386
+
387
+ /** Context handed to an experience plugin around the feature's mount lifecycle. */
388
+ export interface FeaturePluginContext {
389
+ /** In-document root the display mode mounted, or \`null\` for windowed modes. */
390
+ element: HTMLElement | null
391
+ /** The display mode the feature was surfaced in. */
392
+ displayMode: FeatureDisplayMode
393
+ }
394
+
395
+ /** Opt-in extension that decorates the feature's mount lifecycle (e.g. transitions). */
396
+ export interface FeatureExperiencePlugin {
397
+ /** Unique plugin name, surfaced in debug logs. */
398
+ name: string
399
+ /**
400
+ * Runs after the feature mounts; may animate it in and return a teardown.
401
+ *
402
+ * @param context - The mounted element and its display mode.
403
+ * @returns An optional teardown invoked on unmount.
404
+ */
405
+ onMount?(context: FeaturePluginContext): void | (() => void)
406
+ /**
407
+ * Runs before the feature unmounts; may defer teardown until an exit animation finishes.
408
+ *
409
+ * @param context - The mounted element and its display mode.
410
+ * @returns Optionally a promise the shell awaits before tearing down.
411
+ */
412
+ onUnmount?(context: FeaturePluginContext): void | Promise<void>
413
+ }
414
+
415
+ /** Details handed to an unresponsive-feature callback when the feature stops responding. */
416
+ export interface FeatureUnresponsiveInfo {
417
+ /** Consecutive missed heartbeats that tripped the watchdog. */
418
+ missedBeats: number
419
+ /** Timestamp (ms) of the last heartbeat received, or \`null\` if none ever arrived. */
420
+ lastBeatAt: number | null
421
+ /** The display mode the unresponsive feature was using. */
422
+ displayMode: FeatureDisplayMode
423
+ /** Closes the feature gracefully. */
424
+ close(): void
425
+ /** Closes the feature and releases all resources. */
426
+ destroy(): void
427
+ }
428
+
429
+ /**
430
+ * Options accepted by \`createFeatureShell\`; anything omitted falls back to the
431
+ * defaults baked in from the feature's build.
432
+ */
433
+ export interface FeatureShellOptions {
434
+ /** Target element (or CSS selector) the embedded feature mounts into. */
435
+ container: string | HTMLElement
436
+ /** Stable identifier for the feature; seeds the broker name surfaced in debug logs. */
437
+ name?: string
438
+ /** How the feature should be surfaced; defaults to \`embedded\`. */
439
+ displayMode?: FeatureDisplayMode
440
+ /** URL of the feature app to load; defaults to the URL baked in from the feature config. */
441
+ url?: string
442
+ /** How an embedded feature is sized; defaults to \`fill\` (the iframe fills its container). */
443
+ embedSizing?: 'fill' | 'content'
444
+ /** How the host reacts when the feature stops responding; defaults to \`emit\`. */
445
+ onUnresponsive?: 'emit' | 'unmount' | ((info: FeatureUnresponsiveInfo) => void)
446
+ /** Whether pressing Escape closes the shell; defaults to \`true\`. */
447
+ closeOnEscape?: boolean
448
+ /** Dialog width in pixels (dialog mode only). */
449
+ dialogWidth?: number
450
+ /** Dialog height in pixels (dialog mode only). */
451
+ dialogHeight?: number
452
+ /** Whether the dialog renders a dimmed backdrop; defaults to \`true\`. */
453
+ dialogOverlay?: boolean
454
+ /** Security envelope to negotiate; defaults to the protocol baked in from the feature's build. */
455
+ protocol?: FeatureSecurityProtocol
456
+ /** Pre-shared key used by the \`v2\` protocol; always supplied by the host, never baked into the connector. */
457
+ sharedKey?: string
458
+ /** Experience plugins wrapped around each mount/unmount. */
459
+ plugins?: readonly FeatureExperiencePlugin[]
460
+ }
461
+
462
+ /** Handle returned by \`createFeatureShell\`, narrowed to the feature's contract. */
463
+ export interface FeatureShellHandle {
464
+ /**
465
+ * Mounts the feature using the merged baked defaults and call-time options.
466
+ *
467
+ * @param options - Per-open overrides layered over the baked and create-time options.
468
+ */
469
+ open(options?: Partial<FeatureShellOptions>): void
470
+ /** Closes the feature gracefully, disconnecting the messaging channel. */
471
+ close(): void
472
+ /** Closes the feature and releases all resources (channel and DOM). */
473
+ destroy(): void
474
+ /**
475
+ * Sends a contract action to the feature.
476
+ *
477
+ * @param type - Action type from the feature contract's accepted list.
478
+ * @param data - Payload matching the action's schema.
479
+ */
480
+ send<T extends HostSendType>(type: T, data?: HostSendPayloads[T]): void
481
+ /**
482
+ * Subscribes to a feature event with its contract-typed payload.
483
+ *
484
+ * @param event - Event type from the feature contract's emitted list.
485
+ * @param handler - Callback invoked with the typed event payload.
486
+ * @returns A function that removes this subscription.
487
+ */
488
+ on<T extends HostEventType>(event: T, handler: (data: HostEventPayloads[T]) => void): () => void
489
+ /**
490
+ * Subscribes to a shell lifecycle event.
491
+ *
492
+ * @param event - Lifecycle event name.
493
+ * @param handler - Callback invoked when the event fires.
494
+ * @returns A function that removes this subscription.
495
+ */
496
+ on(event: 'open' | 'close' | 'error', handler: (data?: unknown) => void): () => void
497
+ /** Whether the feature channel is currently open (\`true\` while connected). */
498
+ readonly isOpen: boolean
499
+ }
500
+ `;
501
+ /**
502
+ * Escapes a contract description for safe embedding inside a JSDoc comment.
503
+ *
504
+ * @param description - The raw description text.
505
+ * @returns The text with any comment terminator defused.
506
+ */
507
+ function escapeDoc(description) {
508
+ return description.split('*/').join('*\\/');
509
+ }
510
+ /**
511
+ * Renders one payload-map member for an action, with its description as JSDoc.
512
+ *
513
+ * @param action - The contract action to render.
514
+ * @returns The interface member source.
515
+ */
516
+ function buildMember(action) {
517
+ const doc = action.description === undefined ? '' : ` /** ${escapeDoc(action.description)} */\n`;
518
+ return `${doc} ${formatKey(action.type)}: ${schemaToType(action.schema, ' ')}`;
519
+ }
520
+ /**
521
+ * Renders a payload-map interface keyed by action type.
522
+ *
523
+ * @param name - The interface identifier.
524
+ * @param doc - The single-line JSDoc summary.
525
+ * @param actions - The contract actions projected into members.
526
+ * @returns The interface declaration source.
527
+ */
528
+ function buildPayloadInterface(name, doc, actions) {
529
+ const body = actions.length === 0 ? '' : `\n${actions.map(buildMember).join('\n')}\n`;
530
+ return `/** ${doc} */\nexport interface ${name} {${body}}`;
531
+ }
532
+ /**
533
+ * Builds the connector's full generated type surface for a feature contract.
534
+ *
535
+ * Emits payload maps and literal action-name unions projected from the
536
+ * contract, plus structural shell option and handle types, so the connector's
537
+ * declarations resolve with no dependencies beyond the DOM lib.
538
+ *
539
+ * @param contract - The feature contract driving the projected types.
540
+ * @returns The type declaration block for the connector entry.
541
+ *
542
+ * @example Projecting the clock contract
543
+ * ```typescript
544
+ * buildConnectorTypes({ emitted: [{ type: 'timeUpdated' }], accepted: [{ type: 'setTimezone' }] })
545
+ * // => source containing "export interface HostSendPayloads { setTimezone: unknown }"
546
+ * ```
547
+ */
548
+ function buildConnectorTypes(contract) {
549
+ return `${buildPayloadInterface('HostSendPayloads', 'Payloads for actions the host can send, keyed by action type from the feature contract.', contract.accepted)}
550
+
551
+ ${buildPayloadInterface('HostEventPayloads', 'Payloads for events the feature emits to the host, keyed by event type from the feature contract.', contract.emitted)}
552
+
553
+ /** Action types the host can send to the feature. */
554
+ export type HostSendType = keyof HostSendPayloads
555
+
556
+ /** Event types the feature emits to the host. */
557
+ export type HostEventType = keyof HostEventPayloads
558
+
559
+ ${STATIC_TYPES}`;
560
+ }
561
+
242
562
  const ENTRY_PATH = 'src/index.ts';
243
563
  const PACKAGE_PATH = 'package.json';
244
564
  const README_PATH = 'README.md';
245
565
  /**
246
566
  * Builds the baked-in default shell options from the resolved config.
247
567
  *
248
- * @param config - The resolved feature config supplying the URL and display defaults.
568
+ * @param config - The resolved feature config supplying the URL, protocol, and display defaults.
249
569
  * @returns A record of options the connector merges under host-supplied overrides.
250
570
  */
251
571
  function buildDefaults(config) {
252
- return { url: config.url, ...(config.display ?? {}) };
572
+ return {
573
+ url: config.url,
574
+ ...(config.protocol !== undefined && { protocol: config.protocol }),
575
+ ...(config.display ?? {}),
576
+ };
253
577
  }
254
578
  /**
255
- * Builds the connector's TypeScript entry source with the contract inlined.
579
+ * Builds the connector's TypeScript entry source with the contract inlined and
580
+ * the contract-projected types exported.
256
581
  *
257
582
  * @param config - The resolved feature config naming the feature and its URL.
258
583
  * @param contract - The validated contract inlined into the connector.
259
584
  * @returns The entry module source as a string.
260
585
  */
261
586
  function buildConnectorEntry(config, contract) {
262
- return `import type { ShellHandle, ShellOptions } from '@hyperfrontend/features/host'
263
- import { createShell } from '@hyperfrontend/features/host'
587
+ return `import { createShell } from '@hyperfrontend/features/host'
264
588
 
265
- /** Inlined contract describing the ${config.name} feature's actions. */
589
+ ${buildConnectorTypes(contract)}
590
+ /** Inlined contract describing the ${config.name} feature's actions, exactly as the feature authored it. */
266
591
  const contract = ${toSourceLiteral(contract)}
267
592
 
268
- /** Default shell options baked in from the feature config. */
269
- const defaults = ${toSourceLiteral(buildDefaults(config))}
593
+ /** Default shell options baked in from the feature's build. */
594
+ const defaults = <const>${toSourceLiteral(buildDefaults(config))}
270
595
 
271
596
  /**
272
597
  * Creates a host-side shell for the ${config.name} feature.
273
598
  *
599
+ * The feature's contract and build-time defaults are baked in, and \`send\`/\`on\`
600
+ * are typed from the contract's actions.
601
+ *
274
602
  * @param options - Host-supplied options (at minimum a \`container\`); these override the baked defaults.
275
- * @returns A shell handle exposing \`open\`, \`close\`, \`destroy\`, \`send\`, \`on\`, and \`isOpen\`.
603
+ * @returns A typed shell handle exposing \`open\`, \`close\`, \`destroy\`, \`send\`, \`on\`, and \`isOpen\`.
276
604
  */
277
- export function createFeatureShell(options: ShellOptions): ShellHandle {
278
- return createShell({ ...defaults, ...options, contract })
605
+ export function createFeatureShell(options: FeatureShellOptions): FeatureShellHandle {
606
+ return <FeatureShellHandle>createShell({ ...defaults, ...options, contract })
279
607
  }
280
608
  `;
281
609
  }
@@ -298,49 +626,108 @@ function buildConnectorPackageJson(config) {
298
626
  };
299
627
  return `${index_cjs_js$3.stringify(manifest, null, 2)}\n`;
300
628
  }
629
+ /**
630
+ * Builds the open-connector warning block for a protocol-`none` build.
631
+ *
632
+ * @param config - The resolved feature config carrying the baked protocol.
633
+ * @returns The warning block, or an empty string for secured builds.
634
+ */
635
+ function buildReadmeWarning(config) {
636
+ if (config.protocol !== 'none') {
637
+ return '';
638
+ }
639
+ return `> **Warning: open connector.** This build uses protocol \`none\`: messages between host and feature travel with no security envelope, so any page that can reach the feature URL can embed and drive it. For production, rebuild the feature with \`--protocol v1\` or \`--protocol v2\`.
640
+
641
+ `;
642
+ }
643
+ /**
644
+ * Builds the security paragraph matching the baked protocol.
645
+ *
646
+ * @param config - The resolved feature config carrying the baked protocol.
647
+ * @returns The security guidance paragraph.
648
+ */
649
+ function buildReadmeSecurity(config) {
650
+ if (config.protocol === 'v2') {
651
+ return "The `v2` security envelope is baked in from the feature's build — do not pass `protocol` yourself. Supply your own pre-shared key via `sharedKey`; a key is never baked into the artifact.";
652
+ }
653
+ if (config.protocol === 'v1') {
654
+ return "The `v1` security envelope is baked in from the feature's build — do not pass `protocol` yourself.";
655
+ }
656
+ if (config.protocol === 'none') {
657
+ return 'This connector was deliberately built open (see the warning above); harden it by rebuilding the feature with a security protocol.';
658
+ }
659
+ return "No security envelope is baked into this connector; pass `protocol: 'v1'` or `protocol: 'v2'` (with your own `sharedKey` for `v2`) when creating the shell.";
660
+ }
661
+ /**
662
+ * Builds the option lines shown inside the quick-start `createFeatureShell` call.
663
+ *
664
+ * @param config - The resolved feature config carrying the baked protocol.
665
+ * @returns Extra option lines, each newline-prefixed, or an empty string.
666
+ */
667
+ function buildReadmeOptions(config) {
668
+ if (config.protocol === 'v2') {
669
+ return "\n sharedKey: 'your-pre-shared-key',";
670
+ }
671
+ if (config.protocol === undefined) {
672
+ return "\n protocol: 'v2',\n sharedKey: 'your-pre-shared-key',";
673
+ }
674
+ return '';
675
+ }
676
+ /**
677
+ * Builds the typed messaging example lines from the contract's first actions.
678
+ *
679
+ * @param contract - The validated feature contract.
680
+ * @returns Messaging example lines ending in a newline, or an empty string.
681
+ */
682
+ function buildReadmeMessaging(contract) {
683
+ const sent = contract.accepted[0];
684
+ const received = contract.emitted[0];
685
+ const sendLine = sent === undefined ? '' : `shell.send('${sent.type}', data) // typed: the payload shape comes from the feature contract\n`;
686
+ const onLine = received === undefined ? '' : `shell.on('${received.type}', (data) => console.log(data)) // typed: data follows the contract\n`;
687
+ return `${sendLine}${onLine}`;
688
+ }
301
689
  /**
302
690
  * Builds the connector's `README.md` documenting the generated install + usage.
303
691
  *
304
692
  * @param config - The resolved feature config naming the feature.
693
+ * @param contract - The validated feature contract driving the typed examples.
305
694
  * @returns The README contents as a string.
306
695
  */
307
- function buildConnectorReadme(config) {
696
+ function buildConnectorReadme(config, contract) {
308
697
  return `# ${config.name}-shell
309
698
 
310
- Generated host connector for the **${config.name}** feature. Self-contained — install it and embed the feature with one call.
699
+ Generated host connector for the **${config.name}** feature. Self-contained — install it and embed the feature with one call. \`send\` and \`on\` are typed from the feature's contract.
311
700
 
312
- \`\`\`typescript
701
+ ${buildReadmeWarning(config)}\`\`\`typescript
313
702
  import { createFeatureShell } from '${config.name}-shell'
314
703
 
315
704
  const shell = createFeatureShell({
316
- container: '#${config.name}',
317
- // Security envelope: 'none' (default) | 'v1' | 'v2'.
318
- // 'v2' authenticates the channel with a pre-shared key.
319
- protocol: 'v2',
320
- sharedKey: 'your-pre-shared-key',
705
+ container: '#${config.name}',${buildReadmeOptions(config)}
321
706
  })
322
707
 
323
708
  // Lifecycle events ('open', 'close', 'error'); on() returns an unsubscribe fn.
324
709
  const unsubscribe = shell.on('open', () => console.log('connected'))
325
710
 
326
- shell.open() // mount the feature in its display mode
327
- shell.send('setTimezone', { tz: 'UTC' }) // send an action the feature accepts
328
- console.log(shell.isOpen) // current connection state
711
+ shell.open() // mount the feature in its display mode
712
+ ${buildReadmeMessaging(contract)}console.log(shell.isOpen) // current connection state
329
713
 
330
714
  unsubscribe()
331
- shell.close() // disconnect gracefully
332
- shell.destroy() // disconnect and release all resources
715
+ shell.close() // disconnect gracefully
716
+ shell.destroy() // disconnect and release all resources
333
717
  \`\`\`
334
718
 
719
+ ${buildReadmeSecurity(config)}
720
+
335
721
  > Regenerated from scratch on every build; do not edit by hand.
336
722
  `;
337
723
  }
338
724
  /**
339
725
  * Stages the complete host connector package into the supplied VFS tree.
340
726
  *
341
- * Emits the entry source, source-level `package.json`, `README.md`, and (via
342
- * {@link generateMetadata}) `metadata.json`. Pure: stages only into `tree` — the
343
- * CLI owns temp-dir creation, bundling, and commit.
727
+ * Emits the entry source (with contract-projected types), source-level
728
+ * `package.json`, `README.md`, and (via {@link generateMetadata})
729
+ * `metadata.json`. Pure: stages only into `tree` — the CLI owns temp-dir
730
+ * creation, bundling, and commit.
344
731
  *
345
732
  * @param config - The resolved feature config.
346
733
  * @param contract - The validated feature contract, inlined into the connector.
@@ -348,13 +735,13 @@ shell.destroy() // disconnect and release all resource
348
735
  *
349
736
  * @example Staging a connector for the clock feature
350
737
  * ```typescript
351
- * generateShell({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock' }, contract, tree)
738
+ * generateShell({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock', protocol: 'v2' }, contract, tree)
352
739
  * ```
353
740
  */
354
741
  function generateShell(config, contract, tree) {
355
742
  tree.write(ENTRY_PATH, buildConnectorEntry(config, contract));
356
743
  tree.write(PACKAGE_PATH, buildConnectorPackageJson(config));
357
- tree.write(README_PATH, buildConnectorReadme(config));
744
+ tree.write(README_PATH, buildConnectorReadme(config, contract));
358
745
  generateMetadata(config, contract, tree);
359
746
  }
360
747
 
@@ -391,6 +778,31 @@ function collectActionListIssues(actions, field, issues) {
391
778
  if (typeof action['type'] !== 'string' || action['type'].length === 0) {
392
779
  issues.push(`"${field}[${index}]" must have a non-empty string "type".`);
393
780
  }
781
+ if (action['respondsWith'] !== undefined && (typeof action['respondsWith'] !== 'string' || action['respondsWith'].length === 0)) {
782
+ issues.push(`"${field}[${index}]" has a "respondsWith" that must be a non-empty string.`);
783
+ }
784
+ });
785
+ }
786
+ /**
787
+ * Collects every `respondsWith` reference that does not name an action in the other direction.
788
+ *
789
+ * A request emitted by one side is answered by an action the same side accepts (and
790
+ * vice versa), so each `respondsWith` must resolve across the contract's directions.
791
+ *
792
+ * @param actions - The already well-formed action list to check.
793
+ * @param field - The field name of `actions`, used to locate problems in messages.
794
+ * @param other - The action list of the opposite direction.
795
+ * @param otherField - The field name of `other`, used in messages.
796
+ * @param issues - The running list of human-readable problems, appended to in place.
797
+ */
798
+ function collectRespondsWithIssues(actions, field, other, otherField, issues) {
799
+ actions.forEach((action, index) => {
800
+ if (action.respondsWith === undefined) {
801
+ return;
802
+ }
803
+ if (!other.some((candidate) => candidate.type === action.respondsWith)) {
804
+ issues.push(`"${field}[${index}]" responds with "${action.respondsWith}", but "${otherField}" has no action of that type.`);
805
+ }
394
806
  });
395
807
  }
396
808
  /**
@@ -416,7 +828,7 @@ function describeType(value) {
416
828
  *
417
829
  * @param contract - The candidate contract, typically parsed from disk.
418
830
  * @returns The validated contract, typed.
419
- * @throws {Error} When the value is not an object, or any action is malformed.
831
+ * @throws {Error} When the value is not an object, any action is malformed, or a `respondsWith` names no action in the other direction.
420
832
  *
421
833
  * @example Validating a parsed contract file
422
834
  * ```typescript
@@ -431,6 +843,12 @@ function validateContract(contract) {
431
843
  const issues = [];
432
844
  collectActionListIssues(contract['emitted'], 'emitted', issues);
433
845
  collectActionListIssues(contract['accepted'], 'accepted', issues);
846
+ if (issues.length === 0) {
847
+ const emitted = contract['emitted'];
848
+ const accepted = contract['accepted'];
849
+ collectRespondsWithIssues(emitted, 'emitted', accepted, 'accepted', issues);
850
+ collectRespondsWithIssues(accepted, 'accepted', emitted, 'emitted', issues);
851
+ }
434
852
  if (issues.length > 0) {
435
853
  throw index_cjs_js.createError(`Invalid contract:\n${issues.map((issue) => ` - ${issue}`).join('\n')}`);
436
854
  }
@@ -571,7 +989,7 @@ function toAbsolute$3(base, path) {
571
989
  * replaces its whole top-level key (no deep merge).
572
990
  *
573
991
  * @param options - The working directory and parsed flags.
574
- * @returns The resolved config, loaded contract, protocol, and source path.
992
+ * @returns The resolved config, loaded contract, protocol (with its explicitness), and source path.
575
993
  * @throws {Error} When the config or contract is missing required keys or malformed.
576
994
  *
577
995
  * @example Resolving from a discovered config plus a flag override
@@ -592,7 +1010,9 @@ async function resolveBuildConfig(options) {
592
1010
  contract: flags.contract ?? loaded['contract'],
593
1011
  };
594
1012
  const config = validateFeatureConfig(merged);
595
- const protocol = flags.protocol ?? loaded['protocol'] ?? 'none';
1013
+ // why: An explicitly chosen protocol (flag or config) is distinguished from the fallback so the build gate can require a deliberate acknowledgment for an open build instead of ever defaulting into one.
1014
+ const chosenProtocol = flags.protocol ?? loaded['protocol'];
1015
+ const protocol = chosenProtocol ?? 'none';
596
1016
  if (typeof protocol !== 'string' || !VALID_PROTOCOLS.includes(protocol)) {
597
1017
  throw index_cjs_js.createError(`Invalid protocol: "${String(protocol)}" (expected none, v1, or v2).`);
598
1018
  }
@@ -603,8 +1023,15 @@ async function resolveBuildConfig(options) {
603
1023
  ...config,
604
1024
  url: flags.url ?? (typeof loaded['url'] === 'string' ? loaded['url'] : '/'),
605
1025
  ...(display !== undefined && { display }),
1026
+ protocol: protocol,
1027
+ };
1028
+ return {
1029
+ config: resolved,
1030
+ contract,
1031
+ protocol: protocol,
1032
+ protocolExplicit: chosenProtocol !== undefined,
1033
+ ...(sourcePath !== null && { sourcePath }),
606
1034
  };
607
- return { config: resolved, contract, protocol: protocol, ...(sourcePath !== null && { sourcePath }) };
608
1035
  }
609
1036
 
610
1037
  /** Exit code for a successful command run. */
@@ -654,9 +1081,12 @@ function defaultPackTarball(packageDir) {
654
1081
  return output.trim().split('\n').pop() ?? '';
655
1082
  }
656
1083
  /**
657
- * Builds the connector: resolve config → generate the host connector into a temp
658
- * dir → bundle via the builder → pack a tarball into `--out`. A `v1`/`v2` security
659
- * protocol is required for production output, and the temp dir is always removed.
1084
+ * Builds the connector: resolve config → generate the host connector into a
1085
+ * hidden staging dir inside the project → bundle via the builder → pack a
1086
+ * tarball into `--out`. A `v1`/`v2` security protocol is required for production
1087
+ * output; an explicit `--protocol none` builds only when paired with
1088
+ * `--allow-open`, acknowledging the open channel. The staging dir is always
1089
+ * removed.
660
1090
  *
661
1091
  * @param options - Flags, working directory, output sinks, and injectable deps.
662
1092
  * @returns The process exit code.
@@ -674,23 +1104,34 @@ async function runBuild(options) {
674
1104
  const packTarball = options.packTarball ?? defaultPackTarball;
675
1105
  let tempDir = null;
676
1106
  try {
677
- const { config, contract, protocol } = await resolveConfig({ cwd, flags });
1107
+ const { config, contract, protocol, protocolExplicit } = await resolveConfig({ cwd, flags });
678
1108
  if (protocol === 'none') {
679
- stderr.write('Build requires a security protocol: pass --protocol v1 or --protocol v2.\n');
680
- return EXIT_ERROR;
1109
+ if (!protocolExplicit) {
1110
+ stderr.write('Build requires a security protocol: pass --protocol v1 or --protocol v2.\n');
1111
+ return EXIT_ERROR;
1112
+ }
1113
+ if (flags.allowOpen !== true) {
1114
+ stderr.write("Building with an explicit protocol 'none' produces an open connector: the channel is unauthenticated and any page can embed and message the feature. Pass --allow-open to acknowledge the risk, or pick --protocol v1 / --protocol v2.\n");
1115
+ return EXIT_ERROR;
1116
+ }
1117
+ stderr.write("Warning: building an open connector (protocol 'none'); the channel carries no security envelope.\n");
681
1118
  }
682
- const out = toAbsolute$2(cwd, flags.out ?? DEFAULT_OUT);
1119
+ // why: The default output nests per connector so the builder's clean step only ever empties this connector's own directory, never a shared dist/ root.
1120
+ const out = toAbsolute$2(cwd, flags.out ?? node_path.join(DEFAULT_OUT, `${config.name}-shell`));
683
1121
  if (flags.dryRun) {
684
1122
  stdout.write(`Would build "${config.name}" → ${out} [dry run]\n`);
685
1123
  return EXIT_OK;
686
1124
  }
687
- tempDir = node_path.join(node_os.tmpdir(), `hf-shell-${config.name.replace(/[^a-z0-9-]/gi, '-')}-${process.pid}`);
1125
+ // why: The staging dir lives inside the consumer project (not the OS temp dir) so module resolution can ascend into the consumer's node_modules and bundle the SDK into a self-contained connector.
1126
+ tempDir = node_path.join(cwd, `.hf-shell-${config.name.replace(/[^a-z0-9-]/gi, '-')}-${process.pid}`);
688
1127
  index_cjs_js$6.createDirectory(tempDir, { recursive: true });
689
1128
  const tree = index_cjs_js$7.createTree(tempDir);
690
1129
  generateShell(config, contract, tree);
691
1130
  tree.write('tsconfig.lib.json', buildTsConfig());
692
1131
  index_cjs_js$7.commitChanges(tree);
693
- await runBuilder({ projectRoot: tempDir, workspaceRoot: tempDir, outputPath: out });
1132
+ // why: The consumer project is the workspace — it holds node_modules (so the SDK bundles in) and the TypeScript binary the declaration pass spawns.
1133
+ await runBuilder({ projectRoot: tempDir, workspaceRoot: cwd, outputPath: out });
1134
+ publishSidecars(tempDir, out);
694
1135
  const tarball = packTarball(out);
695
1136
  stdout.write(`Built "${config.name}" → ${out}\n${tarball ? `Packed ${tarball}\n` : ''}`);
696
1137
  return EXIT_OK;
@@ -704,6 +1145,23 @@ async function runBuild(options) {
704
1145
  index_cjs_js$6.removeDirectory(tempDir, { recursive: true, force: true });
705
1146
  }
706
1147
  }
1148
+ /**
1149
+ * Copies the staged consumer-facing sidecars (`README.md`, `metadata.json`)
1150
+ * into the built package and lists the metadata file in the manifest's `files`
1151
+ * array so `npm pack` ships them with the connector.
1152
+ *
1153
+ * @param tempDir - The staging directory holding the generated sidecars.
1154
+ * @param out - The built package directory the tarball is packed from.
1155
+ */
1156
+ function publishSidecars(tempDir, out) {
1157
+ index_cjs_js$6.writeFileContent(node_path.join(out, 'README.md'), index_cjs_js$6.readFileContent(node_path.join(tempDir, 'README.md')));
1158
+ index_cjs_js$6.writeFileContent(node_path.join(out, 'metadata.json'), index_cjs_js$6.readFileContent(node_path.join(tempDir, 'metadata.json')));
1159
+ const manifestPath = node_path.join(out, 'package.json');
1160
+ const manifest = index_cjs_js$6.readJsonFileIfExists(manifestPath);
1161
+ if (manifest !== null && index_cjs_js$4.isArray(manifest['files'])) {
1162
+ index_cjs_js$6.writeJsonFile(manifestPath, { ...manifest, files: [...manifest['files'], 'metadata.json'] });
1163
+ }
1164
+ }
707
1165
  /**
708
1166
  * Builds the minimal `tsconfig.lib.json` the builder reads for declarations.
709
1167
  *
@@ -715,6 +1173,8 @@ function buildTsConfig() {
715
1173
  target: 'ES2022',
716
1174
  module: 'ESNext',
717
1175
  moduleResolution: 'Bundler',
1176
+ // why: An explicit rootDir anchors the compiler's file matching to the staged sources, keeping the build correct no matter which directory the CLI is invoked from.
1177
+ rootDir: 'src',
718
1178
  declaration: true,
719
1179
  strict: true,
720
1180
  skipLibCheck: true,
@@ -898,6 +1358,29 @@ async function resolveDevConfig(options) {
898
1358
  };
899
1359
  }
900
1360
 
1361
+ /**
1362
+ * Directory of the currently-running module under either module system.
1363
+ *
1364
+ * The CommonJS build (and any CommonJS host) exposes `__dirname`; the
1365
+ * ES-module build derives the directory from `import.meta.url`. Both formats
1366
+ * ship beside the `debug-ui` assets, so the result anchors asset
1367
+ * self-location wherever the package is installed or embedded.
1368
+ *
1369
+ * @returns Absolute directory path of the running module.
1370
+ *
1371
+ * @example Anchoring an asset lookup beside the running module
1372
+ * ```typescript
1373
+ * const assetDir = join(currentModuleDir(), 'debug-ui')
1374
+ * ```
1375
+ */
1376
+ function currentModuleDir() {
1377
+ // why: `typeof` guards the CommonJS-only global so the ES-module build falls through to `import.meta.url` instead of crashing on an undefined identifier.
1378
+ if (typeof __dirname !== 'undefined') {
1379
+ return __dirname;
1380
+ }
1381
+ return node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('hf.js', document.baseURI).href))));
1382
+ }
1383
+
901
1384
  // note: Minimal MIME table for the asset types a compiled feature/debug-UI bundle ships; anything else falls back to octet-stream.
902
1385
  const CONTENT_TYPES = index_cjs_js$5.freeze({
903
1386
  '.html': 'text/html; charset=utf-8',
@@ -1006,23 +1489,66 @@ function createStaticHandler(root, deps = {}) {
1006
1489
  return (req, res) => serveFile(root, req.url ?? '/', res, deps);
1007
1490
  }
1008
1491
 
1009
- // note: Path prefix the control server serves the compiled debug-UI assets under; the page loads `/__debug/bootstrap.js`.
1492
+ // note: Path prefix the control server serves the compiled debug-UI assets under; the page loads `/__debug/index.iife.min.js`.
1010
1493
  const DEBUG_PREFIX = '/__debug';
1011
1494
  // note: Placeholder the debug-UI HTML embeds; the control server swaps it for the live JSON manifest at request time.
1012
1495
  const MANIFEST_TOKEN = 'HF_MANIFEST_PLACEHOLDER';
1013
1496
  /**
1014
- * Resolves the directory the compiled debug-UI assets ship in, located beside
1015
- * this module via `__dirname` so the server finds them wherever the package is
1016
- * installed.
1497
+ * Probes a single directory for the shipped debug-UI assets, checking both a
1498
+ * sibling `debug-ui` folder (running from the `server/` output) and a nested
1499
+ * `server/debug-ui` folder (running from another package entry, such as the
1500
+ * CLI bin at the package root).
1501
+ *
1502
+ * @param dir - Candidate directory expected to hold the assets.
1503
+ * @param isFile - File probe used to confirm the assets exist.
1504
+ * @returns The `debug-ui` asset directory, or `undefined` when it is not here.
1505
+ */
1506
+ function probeAssetDir(dir, isFile) {
1507
+ const sibling = node_path.join(dir, 'debug-ui');
1508
+ if (isFile(node_path.join(sibling, 'index.html'))) {
1509
+ return sibling;
1510
+ }
1511
+ const nested = node_path.join(dir, 'server', 'debug-ui');
1512
+ return isFile(node_path.join(nested, 'index.html')) ? nested : undefined;
1513
+ }
1514
+ /**
1515
+ * Locates the debug-UI assets by ascending from a start directory toward the
1516
+ * filesystem root, returning the first ancestor holding a `debug-ui` folder.
1517
+ *
1518
+ * @param startDir - Directory the ascent begins from.
1519
+ * @param isFile - File probe used to confirm the assets exist.
1520
+ * @returns The `debug-ui` asset directory, or `undefined` when no ancestor holds one.
1521
+ */
1522
+ function ascendForAssets(startDir, isFile) {
1523
+ let dir = startDir;
1524
+ let parent = node_path.dirname(dir);
1525
+ // how: probe each level then step up; the final probe covers the filesystem root, where parent === dir
1526
+ while (parent !== dir) {
1527
+ const found = probeAssetDir(dir, isFile);
1528
+ if (found !== undefined) {
1529
+ return found;
1530
+ }
1531
+ dir = parent;
1532
+ parent = node_path.dirname(dir);
1533
+ }
1534
+ return probeAssetDir(dir, isFile);
1535
+ }
1536
+ /**
1537
+ * Resolves the directory the compiled debug-UI assets ship in by ascending
1538
+ * from the running module (located via `__dirname` in CommonJS or
1539
+ * `import.meta.url` in ESM). The ascent finds the assets wherever the package
1540
+ * lives — the built dist, an installed `node_modules` copy, or embedded
1541
+ * inside a consumer's bundle output.
1017
1542
  *
1543
+ * @param isFile - File probe used to confirm the assets exist.
1018
1544
  * @returns The absolute debug-UI asset directory.
1019
1545
  */
1020
- function defaultAssetRoot() {
1021
- /* istanbul ignore if -- @preserve the ESM build has no __dirname; the shipped CommonJS build and the test runner always define it */
1022
- if (typeof __dirname === 'undefined') {
1023
- throw index_cjs_js.createError('@hyperfrontend/features dev server self-location requires the CommonJS build; inject `assetRoot` otherwise.');
1546
+ function defaultAssetRoot(isFile) {
1547
+ const located = ascendForAssets(currentModuleDir(), isFile);
1548
+ if (located === undefined) {
1549
+ throw index_cjs_js.createError('@hyperfrontend/features dev server could not locate the bundled debug-UI assets beside the running module; inject `assetRoot` to point at a directory containing the debug-UI `index.html`.');
1024
1550
  }
1025
- return node_path.join(__dirname, 'debug-ui');
1551
+ return located;
1026
1552
  }
1027
1553
  /**
1028
1554
  * Reads the listening port from a server, throwing if the address is unexpectedly absent.
@@ -1133,7 +1659,6 @@ function controlHandler(manifest, assetRoot, deps) {
1133
1659
  */
1134
1660
  async function startDevServer(config, deps = {}) {
1135
1661
  const createServer = deps.createServer ?? node_http.createServer;
1136
- const assetRoot = deps.assetRoot ?? defaultAssetRoot();
1137
1662
  const servers = [];
1138
1663
  const apps = [];
1139
1664
  for (const app of config.apps) {
@@ -1145,6 +1670,8 @@ async function startDevServer(config, deps = {}) {
1145
1670
  const manifest = { apps: apps.map((app) => ({ name: app.name, url: app.url })), debug: config.debug };
1146
1671
  let debugUrl;
1147
1672
  if (config.debug.enabled) {
1673
+ // why: resolved only when the debug UI is enabled so consumers with it disabled never pay for (or fail on) asset self-location.
1674
+ const assetRoot = deps.assetRoot ?? defaultAssetRoot(deps.isFile ?? index_cjs_js$6.isFile);
1148
1675
  const control = createServer(controlHandler(manifest, assetRoot, deps));
1149
1676
  const port = await listen(control, config.debugPort);
1150
1677
  servers.push(control);
@@ -1158,10 +1685,46 @@ async function startDevServer(config, deps = {}) {
1158
1685
  };
1159
1686
  }
1160
1687
 
1688
+ /**
1689
+ * Resolve once the process receives an interrupt or terminate signal.
1690
+ *
1691
+ * Used by the dev command and long-running executors to stay alive after
1692
+ * startup and tear down gracefully when the user (or the task runner) stops
1693
+ * them.
1694
+ *
1695
+ * @returns A promise that settles when `SIGINT` or `SIGTERM` is received.
1696
+ *
1697
+ * @example Stay alive until the target is stopped
1698
+ * ```ts
1699
+ * await waitForShutdown()
1700
+ * await handle.close()
1701
+ * ```
1702
+ */
1703
+ function waitForShutdown() {
1704
+ return index_cjs_js$a.createPromise((resolve) => {
1705
+ for (const signal of ['SIGINT', 'SIGTERM']) {
1706
+ process.once(signal, () => resolve());
1707
+ }
1708
+ });
1709
+ }
1710
+
1711
+ /**
1712
+ * Keeps the dev server running until a termination signal arrives, then closes
1713
+ * every server so the command can exit cleanly.
1714
+ *
1715
+ * @param handle - The running dev server to close once a signal is received.
1716
+ * @returns A promise that resolves after all servers have closed.
1717
+ */
1718
+ async function holdUntilShutdown(handle) {
1719
+ await waitForShutdown();
1720
+ await handle.close();
1721
+ }
1161
1722
  /**
1162
1723
  * Resolves the `hf-dev.config.*` through the shared tiered loader and starts the
1163
- * dev server: one static server per app plus the debug UI. The returned promise
1164
- * resolves once the servers are listening; the process stays alive serving them.
1724
+ * dev server: one static server per app plus the debug UI. After printing the
1725
+ * server URLs the returned promise stays pending while the servers run; it
1726
+ * resolves with the success code once a shutdown signal (`SIGINT`/`SIGTERM`,
1727
+ * e.g. Ctrl-C) arrives and every server has closed cleanly.
1165
1728
  *
1166
1729
  * @param options - Flags, working directory, output sinks, and injectable deps.
1167
1730
  * @returns The process exit code.
@@ -1176,6 +1739,7 @@ async function runDev(options) {
1176
1739
  const cwd = flags.cwd ? node_path.resolve(options.cwd, flags.cwd) : options.cwd;
1177
1740
  const resolveConfig = options.resolveConfig ?? resolveDevConfig;
1178
1741
  const startServer = options.startServer ?? startDevServer;
1742
+ const waitForClose = options.waitForClose ?? holdUntilShutdown;
1179
1743
  try {
1180
1744
  const config = await resolveConfig({ cwd, flags });
1181
1745
  const handle = await startServer(config);
@@ -1183,6 +1747,7 @@ async function runDev(options) {
1183
1747
  if (handle.debugUrl !== undefined) {
1184
1748
  stdout.write(`Debug UI: ${handle.debugUrl}\n`);
1185
1749
  }
1750
+ await waitForClose(handle);
1186
1751
  return EXIT_OK;
1187
1752
  }
1188
1753
  catch (error) {
@@ -1194,25 +1759,32 @@ async function runDev(options) {
1194
1759
  // note: Write-once target; re-runs never clobber handlers the author has filled in.
1195
1760
  const MODULE_PATH = 'src/hyperfrontend.feature.ts';
1196
1761
  /**
1197
- * Derives the extensionless, relative import specifier for the contract module.
1762
+ * Derives the extensionless import specifier for the contract module, computed
1763
+ * relative to the directory the integration module is written into.
1198
1764
  *
1199
- * @param contractPath - The config's contract path (e.g. `contracts/clock.contract.json`).
1200
- * @returns A relative specifier suitable for an `import` statement.
1765
+ * @param contractPath - The config's contract path, tree-root-relative or absolute.
1766
+ * @param treeRoot - Absolute root of the tree the integration module is staged into.
1767
+ * @returns A POSIX-style relative specifier suitable for an `import` statement.
1201
1768
  */
1202
- function toContractImportPath(contractPath) {
1769
+ function toContractImportPath(contractPath, treeRoot) {
1203
1770
  const withoutExtension = contractPath.replace(/\.(json|ts|js)$/, '');
1204
- return /^[./]/.test(withoutExtension) ? withoutExtension : `./${withoutExtension}`;
1771
+ const target = node_path.isAbsolute(withoutExtension) ? withoutExtension : node_path.join(treeRoot, withoutExtension);
1772
+ const specifier = node_path.relative(node_path.join(treeRoot, node_path.dirname(MODULE_PATH)), target)
1773
+ .split('\\')
1774
+ .join('/');
1775
+ return specifier.startsWith('.') ? specifier : `./${specifier}`;
1205
1776
  }
1206
1777
  /**
1207
1778
  * Builds the feature integration module source from the contract.
1208
1779
  *
1209
1780
  * @param config - The resolved feature config naming the feature and contract path.
1210
1781
  * @param contract - The validated contract whose actions drive the scaffolded stubs.
1782
+ * @param treeRoot - Absolute root the config's contract path is relative to.
1211
1783
  * @returns The integration module source as a string.
1212
1784
  */
1213
- function buildFeatureModule(config, contract) {
1785
+ function buildFeatureModule(config, contract, treeRoot) {
1214
1786
  const handlers = contract.accepted
1215
- .map((action) => `feature.on('${action.type}', (data) => {\n // todo: handle ${action.type}\n})`)
1787
+ .map((action) => `feature.on('${action.type}', (_data: unknown) => {\n // todo: handle ${action.type}\n})`)
1216
1788
  .join('\n\n');
1217
1789
  const emits = contract.emitted.map((action) => `// feature.send('${action.type}', undefined)`).join('\n');
1218
1790
  return `/**
@@ -1225,7 +1797,7 @@ function buildFeatureModule(config, contract) {
1225
1797
  * @module ${config.name}.feature
1226
1798
  */
1227
1799
  import { createFeature } from '@hyperfrontend/features/hostee'
1228
- import contract from '${toContractImportPath(config.contract)}'
1800
+ import contract from '${toContractImportPath(config.contract, treeRoot)}'
1229
1801
 
1230
1802
  /** The ${config.name} feature handle; use it to send and receive contract actions. */
1231
1803
  export const feature = createFeature({ name: '${config.name}', contract })
@@ -1254,7 +1826,7 @@ ${emits}
1254
1826
  * ```
1255
1827
  */
1256
1828
  function generateFeatureModule(config, contract, tree) {
1257
- tree.write(MODULE_PATH, buildFeatureModule(config, contract), { mode: index_cjs_js$7.Mode.SkipIfExists });
1829
+ tree.write(MODULE_PATH, buildFeatureModule(config, contract, tree.root), { mode: index_cjs_js$7.Mode.SkipIfExists });
1258
1830
  }
1259
1831
 
1260
1832
  // note: Token searched for to keep insertion idempotent; deleting the block reverses the mutation.
@@ -1538,7 +2110,7 @@ Usage: hf <command> [options]
1538
2110
  Commands:
1539
2111
  init Scaffold the feature glue module and wire it into your app
1540
2112
  build Generate the host connector, bundle it, and pack a tarball
1541
- dev Resolve the dev-server config and start the debug UI
2113
+ dev Start the app servers and debug UI, serving until Ctrl-C
1542
2114
 
1543
2115
  Options:
1544
2116
  --name <name> Feature name
@@ -1547,6 +2119,7 @@ Options:
1547
2119
  --entry <path> Entry file to wire the glue import into (init)
1548
2120
  --url <url> URL the connector loads the feature from (build)
1549
2121
  --protocol <none|v1|v2> Security envelope enforced at build time
2122
+ --allow-open Acknowledge an explicit '--protocol none' and build an open, unauthenticated connector (build)
1550
2123
  --out <dir> Output directory for the built connector (build)
1551
2124
  --apps <path> Path to the dev-server apps array (dev)
1552
2125
  --port <number> Port the dev-server debug UI listens on (dev)