@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/cli/index.esm.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import { createError } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/error/index.esm.js';
2
2
  import { execFileSync } from 'node:child_process';
3
- import { tmpdir } from 'node:os';
4
3
  import { join, dirname, isAbsolute, resolve, normalize, sep, extname, relative } from 'node:path';
5
4
  import { build } from '../_dependencies/@hyperfrontend/builder/index.esm.js';
5
+ import { isArray } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.esm.js';
6
6
  import { stringify } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/json/index.esm.js';
7
- import { exists, readJsonFile, createDirectory, removeDirectory, readFileBuffer, isFile } from '../_dependencies/@hyperfrontend/project-scope/core/fs/index.esm.js';
7
+ import { exists, readJsonFile, createDirectory, removeDirectory, writeFileContent, readFileContent, readJsonFileIfExists, writeJsonFile, readFileBuffer, isFile } from '../_dependencies/@hyperfrontend/project-scope/core/fs/index.esm.js';
8
8
  import { createTree, commitChanges, Mode } from '../_dependencies/@hyperfrontend/project-scope/vfs/index.esm.js';
9
9
  import { format } from '../_dependencies/@hyperfrontend/versioning/semver/format/index.esm.js';
10
10
  import { parseVersionStrict } from '../_dependencies/@hyperfrontend/versioning/semver/parse/index.esm.js';
11
- import { isArray } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.esm.js';
12
11
  import { entries, freeze } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.esm.js';
13
12
  import '../_dependencies/@hyperfrontend/json-utils/index.esm.js';
14
- import { pathToFileURL } from 'node:url';
13
+ import { pathToFileURL, fileURLToPath } from 'node:url';
15
14
  import { isInteger } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/number/index.esm.js';
16
15
  import { createServer } from 'node:http';
17
16
  import { promiseAll, createPromise } from '../_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.esm.js';
18
17
  import { discoverEntryPoints } from '../_dependencies/@hyperfrontend/project-scope/heuristics/entry-points/index.esm.js';
19
18
  import { text, PromptResult, select } from '../_dependencies/@hyperfrontend/questions/index.esm.js';
20
19
  import { generateMetadata } from '../_shared/generators/metadata/generate-metadata/index.esm.js';
21
- import { toSourceLiteral } from '../_shared/generators/shell/source-literal/index.esm.js';
20
+ import { quoteString, formatKey, toSourceLiteral } from '../_shared/generators/shell/source-literal/index.esm.js';
22
21
  import { FEATURE_CONFIG_BASENAME, DEV_CONFIG_BASENAME, discoverConfigFile } from '../_shared/cli/config/discover/index.esm.js';
23
22
  import { loadModuleFile } from '../_shared/cli/config/load-module/index.esm.js';
24
23
  import { EXIT_OK, EXIT_ERROR, EXIT_CANCELLED } from '../_shared/cli/exit-codes/index.esm.js';
24
+ import { waitForShutdown } from '../_shared/shared/shutdown/index.esm.js';
25
25
  import { generateFeatureModule } from '../_shared/generators/feature/generate-feature-module/index.esm.js';
26
26
  import { insertFeatureImport } from '../_shared/cli/insert-marker/index.esm.js';
27
27
  import { promptFeatureName, promptContractPath, promptEntryFile } from '../_shared/cli/prompts/index.esm.js';
@@ -42,6 +42,7 @@ const STRING_FLAGS = {
42
42
  };
43
43
  /** Boolean flags mapped to their {@link CliFlags} key. */
44
44
  const BOOLEAN_FLAGS = {
45
+ '--allow-open': 'allowOpen',
45
46
  '--ci': 'ci',
46
47
  '--yes': 'yes',
47
48
  '--dry-run': 'dryRun',
@@ -91,6 +92,7 @@ function parseCliArgs(argv) {
91
92
  }
92
93
  const flags = {
93
94
  ...strings,
95
+ allowOpen: booleans['allowOpen'] ?? false,
94
96
  ci: booleans['ci'] ?? false,
95
97
  yes: booleans['yes'] ?? false,
96
98
  dryRun: booleans['dryRun'] ?? false,
@@ -99,43 +101,357 @@ function parseCliArgs(argv) {
99
101
  return { ...(command !== undefined && { command }), flags };
100
102
  }
101
103
 
104
+ // note: JSON-schema scalar types mapped straight to their TypeScript equivalents; `integer` narrows to `number`.
105
+ const PRIMITIVES = {
106
+ string: 'string',
107
+ number: 'number',
108
+ integer: 'number',
109
+ boolean: 'boolean',
110
+ null: 'null',
111
+ };
112
+ /**
113
+ * Narrows an unknown value to a plain record.
114
+ *
115
+ * @param value - The value to test.
116
+ * @returns `true` when the value is a non-null, non-array object.
117
+ */
118
+ function isRecord$3(value) {
119
+ return typeof value === 'object' && value !== null && !isArray(value);
120
+ }
121
+ /**
122
+ * Renders a JSON scalar as a TypeScript literal type.
123
+ *
124
+ * @param value - The candidate literal.
125
+ * @returns The literal type source, or `null` for a non-scalar value.
126
+ */
127
+ function literalType(value) {
128
+ if (value === null) {
129
+ return 'null';
130
+ }
131
+ if (typeof value === 'string') {
132
+ return quoteString(value);
133
+ }
134
+ if (typeof value === 'number' || typeof value === 'boolean') {
135
+ return stringify(value);
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Renders an `enum` member list as a union of literal types.
141
+ *
142
+ * @param members - The allowed values listed by the schema.
143
+ * @returns The union source, or `unknown` when any member is not a scalar.
144
+ */
145
+ function enumType(members) {
146
+ const literals = members.map(literalType);
147
+ // 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.
148
+ return literals.every((literal) => literal !== null) ? literals.join(' | ') : 'unknown';
149
+ }
150
+ /**
151
+ * Renders an `object` schema as an inline type literal.
152
+ *
153
+ * @param schema - The schema record with optional `properties` and `required`.
154
+ * @param indent - The current indentation prefix.
155
+ * @returns The object type source; `Record<string, unknown>` without properties.
156
+ */
157
+ function objectType(schema, indent) {
158
+ const properties = isRecord$3(schema['properties']) ? entries(schema['properties']) : [];
159
+ if (properties.length === 0) {
160
+ return 'Record<string, unknown>';
161
+ }
162
+ const required = isArray(schema['required']) ? schema['required'] : [];
163
+ const inner = `${indent} `;
164
+ const members = properties.map(([key, member]) => `${inner}${formatKey(key)}${required.includes(key) ? '' : '?'}: ${schemaToType(member, inner)}`);
165
+ return `{\n${members.join('\n')}\n${indent}}`;
166
+ }
167
+ /**
168
+ * Renders an `array` schema as an element-type array.
169
+ *
170
+ * @param schema - The schema record with an optional single `items` schema.
171
+ * @param indent - The current indentation prefix.
172
+ * @returns The array type source; `unknown[]` without a single `items` schema.
173
+ */
174
+ function arrayType(schema, indent) {
175
+ const items = schema['items'];
176
+ if (!isRecord$3(items)) {
177
+ return 'unknown[]';
178
+ }
179
+ const element = schemaToType(items, indent);
180
+ return element.includes('|') ? `(${element})[]` : `${element}[]`;
181
+ }
182
+ /**
183
+ * Projects a JSON-schema-like payload description into TypeScript type source.
184
+ *
185
+ * Bounded mapping: scalar `type`s, `object` with `properties`/`required`,
186
+ * `array` with a single `items` schema, `enum`, and `const` are projected;
187
+ * anything else (including a missing schema) falls back to `unknown`, so a
188
+ * generated type is never wrong — at worst it is loose.
189
+ *
190
+ * @param schema - The schema value to project.
191
+ * @param indent - Indentation prefix applied to nested object members.
192
+ * @returns The TypeScript type as source text.
193
+ *
194
+ * @example Projecting an action payload schema
195
+ * ```typescript
196
+ * schemaToType({ type: 'object', properties: { tz: { type: 'string' } }, required: ['tz'] })
197
+ * // => '{\n tz: string\n}'
198
+ * ```
199
+ */
200
+ function schemaToType(schema, indent = '') {
201
+ if (!isRecord$3(schema)) {
202
+ return 'unknown';
203
+ }
204
+ const enumMembers = schema['enum'];
205
+ if (isArray(enumMembers) && enumMembers.length > 0) {
206
+ return enumType(enumMembers);
207
+ }
208
+ if ('const' in schema) {
209
+ return literalType(schema['const']) ?? 'unknown';
210
+ }
211
+ const type = schema['type'];
212
+ if (typeof type !== 'string') {
213
+ return 'unknown';
214
+ }
215
+ const primitive = PRIMITIVES[type];
216
+ if (primitive !== undefined) {
217
+ return primitive;
218
+ }
219
+ if (type === 'object') {
220
+ return objectType(schema, indent);
221
+ }
222
+ if (type === 'array') {
223
+ return arrayType(schema, indent);
224
+ }
225
+ return 'unknown';
226
+ }
227
+
228
+ // 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.
229
+ const STATIC_TYPES = `/** How the feature is surfaced by the host. */
230
+ export type FeatureDisplayMode = 'embedded' | 'dialog' | 'popup' | 'standalone'
231
+
232
+ /** Security envelope selector negotiated between host and feature. */
233
+ export type FeatureSecurityProtocol = 'none' | 'v1' | 'v2'
234
+
235
+ /** Context handed to an experience plugin around the feature's mount lifecycle. */
236
+ export interface FeaturePluginContext {
237
+ /** In-document root the display mode mounted, or \`null\` for windowed modes. */
238
+ element: HTMLElement | null
239
+ /** The display mode the feature was surfaced in. */
240
+ displayMode: FeatureDisplayMode
241
+ }
242
+
243
+ /** Opt-in extension that decorates the feature's mount lifecycle (e.g. transitions). */
244
+ export interface FeatureExperiencePlugin {
245
+ /** Unique plugin name, surfaced in debug logs. */
246
+ name: string
247
+ /**
248
+ * Runs after the feature mounts; may animate it in and return a teardown.
249
+ *
250
+ * @param context - The mounted element and its display mode.
251
+ * @returns An optional teardown invoked on unmount.
252
+ */
253
+ onMount?(context: FeaturePluginContext): void | (() => void)
254
+ /**
255
+ * Runs before the feature unmounts; may defer teardown until an exit animation finishes.
256
+ *
257
+ * @param context - The mounted element and its display mode.
258
+ * @returns Optionally a promise the shell awaits before tearing down.
259
+ */
260
+ onUnmount?(context: FeaturePluginContext): void | Promise<void>
261
+ }
262
+
263
+ /** Details handed to an unresponsive-feature callback when the feature stops responding. */
264
+ export interface FeatureUnresponsiveInfo {
265
+ /** Consecutive missed heartbeats that tripped the watchdog. */
266
+ missedBeats: number
267
+ /** Timestamp (ms) of the last heartbeat received, or \`null\` if none ever arrived. */
268
+ lastBeatAt: number | null
269
+ /** The display mode the unresponsive feature was using. */
270
+ displayMode: FeatureDisplayMode
271
+ /** Closes the feature gracefully. */
272
+ close(): void
273
+ /** Closes the feature and releases all resources. */
274
+ destroy(): void
275
+ }
276
+
277
+ /**
278
+ * Options accepted by \`createFeatureShell\`; anything omitted falls back to the
279
+ * defaults baked in from the feature's build.
280
+ */
281
+ export interface FeatureShellOptions {
282
+ /** Target element (or CSS selector) the embedded feature mounts into. */
283
+ container: string | HTMLElement
284
+ /** Stable identifier for the feature; seeds the broker name surfaced in debug logs. */
285
+ name?: string
286
+ /** How the feature should be surfaced; defaults to \`embedded\`. */
287
+ displayMode?: FeatureDisplayMode
288
+ /** URL of the feature app to load; defaults to the URL baked in from the feature config. */
289
+ url?: string
290
+ /** How an embedded feature is sized; defaults to \`fill\` (the iframe fills its container). */
291
+ embedSizing?: 'fill' | 'content'
292
+ /** How the host reacts when the feature stops responding; defaults to \`emit\`. */
293
+ onUnresponsive?: 'emit' | 'unmount' | ((info: FeatureUnresponsiveInfo) => void)
294
+ /** Whether pressing Escape closes the shell; defaults to \`true\`. */
295
+ closeOnEscape?: boolean
296
+ /** Dialog width in pixels (dialog mode only). */
297
+ dialogWidth?: number
298
+ /** Dialog height in pixels (dialog mode only). */
299
+ dialogHeight?: number
300
+ /** Whether the dialog renders a dimmed backdrop; defaults to \`true\`. */
301
+ dialogOverlay?: boolean
302
+ /** Security envelope to negotiate; defaults to the protocol baked in from the feature's build. */
303
+ protocol?: FeatureSecurityProtocol
304
+ /** Pre-shared key used by the \`v2\` protocol; always supplied by the host, never baked into the connector. */
305
+ sharedKey?: string
306
+ /** Experience plugins wrapped around each mount/unmount. */
307
+ plugins?: readonly FeatureExperiencePlugin[]
308
+ }
309
+
310
+ /** Handle returned by \`createFeatureShell\`, narrowed to the feature's contract. */
311
+ export interface FeatureShellHandle {
312
+ /**
313
+ * Mounts the feature using the merged baked defaults and call-time options.
314
+ *
315
+ * @param options - Per-open overrides layered over the baked and create-time options.
316
+ */
317
+ open(options?: Partial<FeatureShellOptions>): void
318
+ /** Closes the feature gracefully, disconnecting the messaging channel. */
319
+ close(): void
320
+ /** Closes the feature and releases all resources (channel and DOM). */
321
+ destroy(): void
322
+ /**
323
+ * Sends a contract action to the feature.
324
+ *
325
+ * @param type - Action type from the feature contract's accepted list.
326
+ * @param data - Payload matching the action's schema.
327
+ */
328
+ send<T extends HostSendType>(type: T, data?: HostSendPayloads[T]): void
329
+ /**
330
+ * Subscribes to a feature event with its contract-typed payload.
331
+ *
332
+ * @param event - Event type from the feature contract's emitted list.
333
+ * @param handler - Callback invoked with the typed event payload.
334
+ * @returns A function that removes this subscription.
335
+ */
336
+ on<T extends HostEventType>(event: T, handler: (data: HostEventPayloads[T]) => void): () => void
337
+ /**
338
+ * Subscribes to a shell lifecycle event.
339
+ *
340
+ * @param event - Lifecycle event name.
341
+ * @param handler - Callback invoked when the event fires.
342
+ * @returns A function that removes this subscription.
343
+ */
344
+ on(event: 'open' | 'close' | 'error', handler: (data?: unknown) => void): () => void
345
+ /** Whether the feature channel is currently open (\`true\` while connected). */
346
+ readonly isOpen: boolean
347
+ }
348
+ `;
349
+ /**
350
+ * Escapes a contract description for safe embedding inside a JSDoc comment.
351
+ *
352
+ * @param description - The raw description text.
353
+ * @returns The text with any comment terminator defused.
354
+ */
355
+ function escapeDoc(description) {
356
+ return description.split('*/').join('*\\/');
357
+ }
358
+ /**
359
+ * Renders one payload-map member for an action, with its description as JSDoc.
360
+ *
361
+ * @param action - The contract action to render.
362
+ * @returns The interface member source.
363
+ */
364
+ function buildMember(action) {
365
+ const doc = action.description === undefined ? '' : ` /** ${escapeDoc(action.description)} */\n`;
366
+ return `${doc} ${formatKey(action.type)}: ${schemaToType(action.schema, ' ')}`;
367
+ }
368
+ /**
369
+ * Renders a payload-map interface keyed by action type.
370
+ *
371
+ * @param name - The interface identifier.
372
+ * @param doc - The single-line JSDoc summary.
373
+ * @param actions - The contract actions projected into members.
374
+ * @returns The interface declaration source.
375
+ */
376
+ function buildPayloadInterface(name, doc, actions) {
377
+ const body = actions.length === 0 ? '' : `\n${actions.map(buildMember).join('\n')}\n`;
378
+ return `/** ${doc} */\nexport interface ${name} {${body}}`;
379
+ }
380
+ /**
381
+ * Builds the connector's full generated type surface for a feature contract.
382
+ *
383
+ * Emits payload maps and literal action-name unions projected from the
384
+ * contract, plus structural shell option and handle types, so the connector's
385
+ * declarations resolve with no dependencies beyond the DOM lib.
386
+ *
387
+ * @param contract - The feature contract driving the projected types.
388
+ * @returns The type declaration block for the connector entry.
389
+ *
390
+ * @example Projecting the clock contract
391
+ * ```typescript
392
+ * buildConnectorTypes({ emitted: [{ type: 'timeUpdated' }], accepted: [{ type: 'setTimezone' }] })
393
+ * // => source containing "export interface HostSendPayloads { setTimezone: unknown }"
394
+ * ```
395
+ */
396
+ function buildConnectorTypes(contract) {
397
+ return `${buildPayloadInterface('HostSendPayloads', 'Payloads for actions the host can send, keyed by action type from the feature contract.', contract.accepted)}
398
+
399
+ ${buildPayloadInterface('HostEventPayloads', 'Payloads for events the feature emits to the host, keyed by event type from the feature contract.', contract.emitted)}
400
+
401
+ /** Action types the host can send to the feature. */
402
+ export type HostSendType = keyof HostSendPayloads
403
+
404
+ /** Event types the feature emits to the host. */
405
+ export type HostEventType = keyof HostEventPayloads
406
+
407
+ ${STATIC_TYPES}`;
408
+ }
409
+
102
410
  const ENTRY_PATH = 'src/index.ts';
103
411
  const PACKAGE_PATH = 'package.json';
104
412
  const README_PATH = 'README.md';
105
413
  /**
106
414
  * Builds the baked-in default shell options from the resolved config.
107
415
  *
108
- * @param config - The resolved feature config supplying the URL and display defaults.
416
+ * @param config - The resolved feature config supplying the URL, protocol, and display defaults.
109
417
  * @returns A record of options the connector merges under host-supplied overrides.
110
418
  */
111
419
  function buildDefaults(config) {
112
- return { url: config.url, ...(config.display ?? {}) };
420
+ return {
421
+ url: config.url,
422
+ ...(config.protocol !== undefined && { protocol: config.protocol }),
423
+ ...(config.display ?? {}),
424
+ };
113
425
  }
114
426
  /**
115
- * Builds the connector's TypeScript entry source with the contract inlined.
427
+ * Builds the connector's TypeScript entry source with the contract inlined and
428
+ * the contract-projected types exported.
116
429
  *
117
430
  * @param config - The resolved feature config naming the feature and its URL.
118
431
  * @param contract - The validated contract inlined into the connector.
119
432
  * @returns The entry module source as a string.
120
433
  */
121
434
  function buildConnectorEntry(config, contract) {
122
- return `import type { ShellHandle, ShellOptions } from '@hyperfrontend/features/host'
123
- import { createShell } from '@hyperfrontend/features/host'
435
+ return `import { createShell } from '@hyperfrontend/features/host'
124
436
 
125
- /** Inlined contract describing the ${config.name} feature's actions. */
437
+ ${buildConnectorTypes(contract)}
438
+ /** Inlined contract describing the ${config.name} feature's actions, exactly as the feature authored it. */
126
439
  const contract = ${toSourceLiteral(contract)}
127
440
 
128
- /** Default shell options baked in from the feature config. */
129
- const defaults = ${toSourceLiteral(buildDefaults(config))}
441
+ /** Default shell options baked in from the feature's build. */
442
+ const defaults = <const>${toSourceLiteral(buildDefaults(config))}
130
443
 
131
444
  /**
132
445
  * Creates a host-side shell for the ${config.name} feature.
133
446
  *
447
+ * The feature's contract and build-time defaults are baked in, and \`send\`/\`on\`
448
+ * are typed from the contract's actions.
449
+ *
134
450
  * @param options - Host-supplied options (at minimum a \`container\`); these override the baked defaults.
135
- * @returns A shell handle exposing \`open\`, \`close\`, \`destroy\`, \`send\`, \`on\`, and \`isOpen\`.
451
+ * @returns A typed shell handle exposing \`open\`, \`close\`, \`destroy\`, \`send\`, \`on\`, and \`isOpen\`.
136
452
  */
137
- export function createFeatureShell(options: ShellOptions): ShellHandle {
138
- return createShell({ ...defaults, ...options, contract })
453
+ export function createFeatureShell(options: FeatureShellOptions): FeatureShellHandle {
454
+ return <FeatureShellHandle>createShell({ ...defaults, ...options, contract })
139
455
  }
140
456
  `;
141
457
  }
@@ -158,49 +474,108 @@ function buildConnectorPackageJson(config) {
158
474
  };
159
475
  return `${stringify(manifest, null, 2)}\n`;
160
476
  }
477
+ /**
478
+ * Builds the open-connector warning block for a protocol-`none` build.
479
+ *
480
+ * @param config - The resolved feature config carrying the baked protocol.
481
+ * @returns The warning block, or an empty string for secured builds.
482
+ */
483
+ function buildReadmeWarning(config) {
484
+ if (config.protocol !== 'none') {
485
+ return '';
486
+ }
487
+ 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\`.
488
+
489
+ `;
490
+ }
491
+ /**
492
+ * Builds the security paragraph matching the baked protocol.
493
+ *
494
+ * @param config - The resolved feature config carrying the baked protocol.
495
+ * @returns The security guidance paragraph.
496
+ */
497
+ function buildReadmeSecurity(config) {
498
+ if (config.protocol === 'v2') {
499
+ 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.";
500
+ }
501
+ if (config.protocol === 'v1') {
502
+ return "The `v1` security envelope is baked in from the feature's build — do not pass `protocol` yourself.";
503
+ }
504
+ if (config.protocol === 'none') {
505
+ return 'This connector was deliberately built open (see the warning above); harden it by rebuilding the feature with a security protocol.';
506
+ }
507
+ 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.";
508
+ }
509
+ /**
510
+ * Builds the option lines shown inside the quick-start `createFeatureShell` call.
511
+ *
512
+ * @param config - The resolved feature config carrying the baked protocol.
513
+ * @returns Extra option lines, each newline-prefixed, or an empty string.
514
+ */
515
+ function buildReadmeOptions(config) {
516
+ if (config.protocol === 'v2') {
517
+ return "\n sharedKey: 'your-pre-shared-key',";
518
+ }
519
+ if (config.protocol === undefined) {
520
+ return "\n protocol: 'v2',\n sharedKey: 'your-pre-shared-key',";
521
+ }
522
+ return '';
523
+ }
524
+ /**
525
+ * Builds the typed messaging example lines from the contract's first actions.
526
+ *
527
+ * @param contract - The validated feature contract.
528
+ * @returns Messaging example lines ending in a newline, or an empty string.
529
+ */
530
+ function buildReadmeMessaging(contract) {
531
+ const sent = contract.accepted[0];
532
+ const received = contract.emitted[0];
533
+ const sendLine = sent === undefined ? '' : `shell.send('${sent.type}', data) // typed: the payload shape comes from the feature contract\n`;
534
+ const onLine = received === undefined ? '' : `shell.on('${received.type}', (data) => console.log(data)) // typed: data follows the contract\n`;
535
+ return `${sendLine}${onLine}`;
536
+ }
161
537
  /**
162
538
  * Builds the connector's `README.md` documenting the generated install + usage.
163
539
  *
164
540
  * @param config - The resolved feature config naming the feature.
541
+ * @param contract - The validated feature contract driving the typed examples.
165
542
  * @returns The README contents as a string.
166
543
  */
167
- function buildConnectorReadme(config) {
544
+ function buildConnectorReadme(config, contract) {
168
545
  return `# ${config.name}-shell
169
546
 
170
- Generated host connector for the **${config.name}** feature. Self-contained — install it and embed the feature with one call.
547
+ 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.
171
548
 
172
- \`\`\`typescript
549
+ ${buildReadmeWarning(config)}\`\`\`typescript
173
550
  import { createFeatureShell } from '${config.name}-shell'
174
551
 
175
552
  const shell = createFeatureShell({
176
- container: '#${config.name}',
177
- // Security envelope: 'none' (default) | 'v1' | 'v2'.
178
- // 'v2' authenticates the channel with a pre-shared key.
179
- protocol: 'v2',
180
- sharedKey: 'your-pre-shared-key',
553
+ container: '#${config.name}',${buildReadmeOptions(config)}
181
554
  })
182
555
 
183
556
  // Lifecycle events ('open', 'close', 'error'); on() returns an unsubscribe fn.
184
557
  const unsubscribe = shell.on('open', () => console.log('connected'))
185
558
 
186
- shell.open() // mount the feature in its display mode
187
- shell.send('setTimezone', { tz: 'UTC' }) // send an action the feature accepts
188
- console.log(shell.isOpen) // current connection state
559
+ shell.open() // mount the feature in its display mode
560
+ ${buildReadmeMessaging(contract)}console.log(shell.isOpen) // current connection state
189
561
 
190
562
  unsubscribe()
191
- shell.close() // disconnect gracefully
192
- shell.destroy() // disconnect and release all resources
563
+ shell.close() // disconnect gracefully
564
+ shell.destroy() // disconnect and release all resources
193
565
  \`\`\`
194
566
 
567
+ ${buildReadmeSecurity(config)}
568
+
195
569
  > Regenerated from scratch on every build; do not edit by hand.
196
570
  `;
197
571
  }
198
572
  /**
199
573
  * Stages the complete host connector package into the supplied VFS tree.
200
574
  *
201
- * Emits the entry source, source-level `package.json`, `README.md`, and (via
202
- * {@link generateMetadata}) `metadata.json`. Pure: stages only into `tree` — the
203
- * CLI owns temp-dir creation, bundling, and commit.
575
+ * Emits the entry source (with contract-projected types), source-level
576
+ * `package.json`, `README.md`, and (via {@link generateMetadata})
577
+ * `metadata.json`. Pure: stages only into `tree` — the CLI owns temp-dir
578
+ * creation, bundling, and commit.
204
579
  *
205
580
  * @param config - The resolved feature config.
206
581
  * @param contract - The validated feature contract, inlined into the connector.
@@ -208,13 +583,13 @@ shell.destroy() // disconnect and release all resource
208
583
  *
209
584
  * @example Staging a connector for the clock feature
210
585
  * ```typescript
211
- * generateShell({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock' }, contract, tree)
586
+ * generateShell({ name: 'clock', version: '1.0.0', contract: './clock.contract.json', url: '/clock', protocol: 'v2' }, contract, tree)
212
587
  * ```
213
588
  */
214
589
  function generateShell(config, contract, tree) {
215
590
  tree.write(ENTRY_PATH, buildConnectorEntry(config, contract));
216
591
  tree.write(PACKAGE_PATH, buildConnectorPackageJson(config));
217
- tree.write(README_PATH, buildConnectorReadme(config));
592
+ tree.write(README_PATH, buildConnectorReadme(config, contract));
218
593
  generateMetadata(config, contract, tree);
219
594
  }
220
595
 
@@ -251,6 +626,31 @@ function collectActionListIssues(actions, field, issues) {
251
626
  if (typeof action['type'] !== 'string' || action['type'].length === 0) {
252
627
  issues.push(`"${field}[${index}]" must have a non-empty string "type".`);
253
628
  }
629
+ if (action['respondsWith'] !== undefined && (typeof action['respondsWith'] !== 'string' || action['respondsWith'].length === 0)) {
630
+ issues.push(`"${field}[${index}]" has a "respondsWith" that must be a non-empty string.`);
631
+ }
632
+ });
633
+ }
634
+ /**
635
+ * Collects every `respondsWith` reference that does not name an action in the other direction.
636
+ *
637
+ * A request emitted by one side is answered by an action the same side accepts (and
638
+ * vice versa), so each `respondsWith` must resolve across the contract's directions.
639
+ *
640
+ * @param actions - The already well-formed action list to check.
641
+ * @param field - The field name of `actions`, used to locate problems in messages.
642
+ * @param other - The action list of the opposite direction.
643
+ * @param otherField - The field name of `other`, used in messages.
644
+ * @param issues - The running list of human-readable problems, appended to in place.
645
+ */
646
+ function collectRespondsWithIssues(actions, field, other, otherField, issues) {
647
+ actions.forEach((action, index) => {
648
+ if (action.respondsWith === undefined) {
649
+ return;
650
+ }
651
+ if (!other.some((candidate) => candidate.type === action.respondsWith)) {
652
+ issues.push(`"${field}[${index}]" responds with "${action.respondsWith}", but "${otherField}" has no action of that type.`);
653
+ }
254
654
  });
255
655
  }
256
656
  /**
@@ -276,7 +676,7 @@ function describeType(value) {
276
676
  *
277
677
  * @param contract - The candidate contract, typically parsed from disk.
278
678
  * @returns The validated contract, typed.
279
- * @throws {Error} When the value is not an object, or any action is malformed.
679
+ * @throws {Error} When the value is not an object, any action is malformed, or a `respondsWith` names no action in the other direction.
280
680
  *
281
681
  * @example Validating a parsed contract file
282
682
  * ```typescript
@@ -291,6 +691,12 @@ function validateContract(contract) {
291
691
  const issues = [];
292
692
  collectActionListIssues(contract['emitted'], 'emitted', issues);
293
693
  collectActionListIssues(contract['accepted'], 'accepted', issues);
694
+ if (issues.length === 0) {
695
+ const emitted = contract['emitted'];
696
+ const accepted = contract['accepted'];
697
+ collectRespondsWithIssues(emitted, 'emitted', accepted, 'accepted', issues);
698
+ collectRespondsWithIssues(accepted, 'accepted', emitted, 'emitted', issues);
699
+ }
294
700
  if (issues.length > 0) {
295
701
  throw createError(`Invalid contract:\n${issues.map((issue) => ` - ${issue}`).join('\n')}`);
296
702
  }
@@ -351,7 +757,7 @@ function toAbsolute$3(base, path) {
351
757
  * replaces its whole top-level key (no deep merge).
352
758
  *
353
759
  * @param options - The working directory and parsed flags.
354
- * @returns The resolved config, loaded contract, protocol, and source path.
760
+ * @returns The resolved config, loaded contract, protocol (with its explicitness), and source path.
355
761
  * @throws {Error} When the config or contract is missing required keys or malformed.
356
762
  *
357
763
  * @example Resolving from a discovered config plus a flag override
@@ -372,7 +778,9 @@ async function resolveBuildConfig(options) {
372
778
  contract: flags.contract ?? loaded['contract'],
373
779
  };
374
780
  const config = validateFeatureConfig(merged);
375
- const protocol = flags.protocol ?? loaded['protocol'] ?? 'none';
781
+ // 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.
782
+ const chosenProtocol = flags.protocol ?? loaded['protocol'];
783
+ const protocol = chosenProtocol ?? 'none';
376
784
  if (typeof protocol !== 'string' || !VALID_PROTOCOLS.includes(protocol)) {
377
785
  throw createError(`Invalid protocol: "${String(protocol)}" (expected none, v1, or v2).`);
378
786
  }
@@ -383,8 +791,15 @@ async function resolveBuildConfig(options) {
383
791
  ...config,
384
792
  url: flags.url ?? (typeof loaded['url'] === 'string' ? loaded['url'] : '/'),
385
793
  ...(display !== undefined && { display }),
794
+ protocol: protocol,
795
+ };
796
+ return {
797
+ config: resolved,
798
+ contract,
799
+ protocol: protocol,
800
+ protocolExplicit: chosenProtocol !== undefined,
801
+ ...(sourcePath !== null && { sourcePath }),
386
802
  };
387
- return { config: resolved, contract, protocol: protocol, ...(sourcePath !== null && { sourcePath }) };
388
803
  }
389
804
 
390
805
  const DEFAULT_OUT = 'dist';
@@ -427,9 +842,12 @@ function defaultPackTarball(packageDir) {
427
842
  return output.trim().split('\n').pop() ?? '';
428
843
  }
429
844
  /**
430
- * Builds the connector: resolve config → generate the host connector into a temp
431
- * dir → bundle via the builder → pack a tarball into `--out`. A `v1`/`v2` security
432
- * protocol is required for production output, and the temp dir is always removed.
845
+ * Builds the connector: resolve config → generate the host connector into a
846
+ * hidden staging dir inside the project → bundle via the builder → pack a
847
+ * tarball into `--out`. A `v1`/`v2` security protocol is required for production
848
+ * output; an explicit `--protocol none` builds only when paired with
849
+ * `--allow-open`, acknowledging the open channel. The staging dir is always
850
+ * removed.
433
851
  *
434
852
  * @param options - Flags, working directory, output sinks, and injectable deps.
435
853
  * @returns The process exit code.
@@ -447,23 +865,34 @@ async function runBuild(options) {
447
865
  const packTarball = options.packTarball ?? defaultPackTarball;
448
866
  let tempDir = null;
449
867
  try {
450
- const { config, contract, protocol } = await resolveConfig({ cwd, flags });
868
+ const { config, contract, protocol, protocolExplicit } = await resolveConfig({ cwd, flags });
451
869
  if (protocol === 'none') {
452
- stderr.write('Build requires a security protocol: pass --protocol v1 or --protocol v2.\n');
453
- return EXIT_ERROR;
870
+ if (!protocolExplicit) {
871
+ stderr.write('Build requires a security protocol: pass --protocol v1 or --protocol v2.\n');
872
+ return EXIT_ERROR;
873
+ }
874
+ if (flags.allowOpen !== true) {
875
+ 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");
876
+ return EXIT_ERROR;
877
+ }
878
+ stderr.write("Warning: building an open connector (protocol 'none'); the channel carries no security envelope.\n");
454
879
  }
455
- const out = toAbsolute$2(cwd, flags.out ?? DEFAULT_OUT);
880
+ // 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.
881
+ const out = toAbsolute$2(cwd, flags.out ?? join(DEFAULT_OUT, `${config.name}-shell`));
456
882
  if (flags.dryRun) {
457
883
  stdout.write(`Would build "${config.name}" → ${out} [dry run]\n`);
458
884
  return EXIT_OK;
459
885
  }
460
- tempDir = join(tmpdir(), `hf-shell-${config.name.replace(/[^a-z0-9-]/gi, '-')}-${process.pid}`);
886
+ // 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.
887
+ tempDir = join(cwd, `.hf-shell-${config.name.replace(/[^a-z0-9-]/gi, '-')}-${process.pid}`);
461
888
  createDirectory(tempDir, { recursive: true });
462
889
  const tree = createTree(tempDir);
463
890
  generateShell(config, contract, tree);
464
891
  tree.write('tsconfig.lib.json', buildTsConfig());
465
892
  commitChanges(tree);
466
- await runBuilder({ projectRoot: tempDir, workspaceRoot: tempDir, outputPath: out });
893
+ // why: The consumer project is the workspace — it holds node_modules (so the SDK bundles in) and the TypeScript binary the declaration pass spawns.
894
+ await runBuilder({ projectRoot: tempDir, workspaceRoot: cwd, outputPath: out });
895
+ publishSidecars(tempDir, out);
467
896
  const tarball = packTarball(out);
468
897
  stdout.write(`Built "${config.name}" → ${out}\n${tarball ? `Packed ${tarball}\n` : ''}`);
469
898
  return EXIT_OK;
@@ -477,6 +906,23 @@ async function runBuild(options) {
477
906
  removeDirectory(tempDir, { recursive: true, force: true });
478
907
  }
479
908
  }
909
+ /**
910
+ * Copies the staged consumer-facing sidecars (`README.md`, `metadata.json`)
911
+ * into the built package and lists the metadata file in the manifest's `files`
912
+ * array so `npm pack` ships them with the connector.
913
+ *
914
+ * @param tempDir - The staging directory holding the generated sidecars.
915
+ * @param out - The built package directory the tarball is packed from.
916
+ */
917
+ function publishSidecars(tempDir, out) {
918
+ writeFileContent(join(out, 'README.md'), readFileContent(join(tempDir, 'README.md')));
919
+ writeFileContent(join(out, 'metadata.json'), readFileContent(join(tempDir, 'metadata.json')));
920
+ const manifestPath = join(out, 'package.json');
921
+ const manifest = readJsonFileIfExists(manifestPath);
922
+ if (manifest !== null && isArray(manifest['files'])) {
923
+ writeJsonFile(manifestPath, { ...manifest, files: [...manifest['files'], 'metadata.json'] });
924
+ }
925
+ }
480
926
  /**
481
927
  * Builds the minimal `tsconfig.lib.json` the builder reads for declarations.
482
928
  *
@@ -488,6 +934,8 @@ function buildTsConfig() {
488
934
  target: 'ES2022',
489
935
  module: 'ESNext',
490
936
  moduleResolution: 'Bundler',
937
+ // 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.
938
+ rootDir: 'src',
491
939
  declaration: true,
492
940
  strict: true,
493
941
  skipLibCheck: true,
@@ -671,6 +1119,29 @@ async function resolveDevConfig(options) {
671
1119
  };
672
1120
  }
673
1121
 
1122
+ /**
1123
+ * Directory of the currently-running module under either module system.
1124
+ *
1125
+ * The CommonJS build (and any CommonJS host) exposes `__dirname`; the
1126
+ * ES-module build derives the directory from `import.meta.url`. Both formats
1127
+ * ship beside the `debug-ui` assets, so the result anchors asset
1128
+ * self-location wherever the package is installed or embedded.
1129
+ *
1130
+ * @returns Absolute directory path of the running module.
1131
+ *
1132
+ * @example Anchoring an asset lookup beside the running module
1133
+ * ```typescript
1134
+ * const assetDir = join(currentModuleDir(), 'debug-ui')
1135
+ * ```
1136
+ */
1137
+ function currentModuleDir() {
1138
+ // 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.
1139
+ if (typeof __dirname !== 'undefined') {
1140
+ return __dirname;
1141
+ }
1142
+ return dirname(fileURLToPath(import.meta.url));
1143
+ }
1144
+
674
1145
  // note: Minimal MIME table for the asset types a compiled feature/debug-UI bundle ships; anything else falls back to octet-stream.
675
1146
  const CONTENT_TYPES = freeze({
676
1147
  '.html': 'text/html; charset=utf-8',
@@ -779,23 +1250,66 @@ function createStaticHandler(root, deps = {}) {
779
1250
  return (req, res) => serveFile(root, req.url ?? '/', res, deps);
780
1251
  }
781
1252
 
782
- // note: Path prefix the control server serves the compiled debug-UI assets under; the page loads `/__debug/bootstrap.js`.
1253
+ // note: Path prefix the control server serves the compiled debug-UI assets under; the page loads `/__debug/index.iife.min.js`.
783
1254
  const DEBUG_PREFIX = '/__debug';
784
1255
  // note: Placeholder the debug-UI HTML embeds; the control server swaps it for the live JSON manifest at request time.
785
1256
  const MANIFEST_TOKEN = 'HF_MANIFEST_PLACEHOLDER';
786
1257
  /**
787
- * Resolves the directory the compiled debug-UI assets ship in, located beside
788
- * this module via `__dirname` so the server finds them wherever the package is
789
- * installed.
1258
+ * Probes a single directory for the shipped debug-UI assets, checking both a
1259
+ * sibling `debug-ui` folder (running from the `server/` output) and a nested
1260
+ * `server/debug-ui` folder (running from another package entry, such as the
1261
+ * CLI bin at the package root).
1262
+ *
1263
+ * @param dir - Candidate directory expected to hold the assets.
1264
+ * @param isFile - File probe used to confirm the assets exist.
1265
+ * @returns The `debug-ui` asset directory, or `undefined` when it is not here.
1266
+ */
1267
+ function probeAssetDir(dir, isFile) {
1268
+ const sibling = join(dir, 'debug-ui');
1269
+ if (isFile(join(sibling, 'index.html'))) {
1270
+ return sibling;
1271
+ }
1272
+ const nested = join(dir, 'server', 'debug-ui');
1273
+ return isFile(join(nested, 'index.html')) ? nested : undefined;
1274
+ }
1275
+ /**
1276
+ * Locates the debug-UI assets by ascending from a start directory toward the
1277
+ * filesystem root, returning the first ancestor holding a `debug-ui` folder.
790
1278
  *
1279
+ * @param startDir - Directory the ascent begins from.
1280
+ * @param isFile - File probe used to confirm the assets exist.
1281
+ * @returns The `debug-ui` asset directory, or `undefined` when no ancestor holds one.
1282
+ */
1283
+ function ascendForAssets(startDir, isFile) {
1284
+ let dir = startDir;
1285
+ let parent = dirname(dir);
1286
+ // how: probe each level then step up; the final probe covers the filesystem root, where parent === dir
1287
+ while (parent !== dir) {
1288
+ const found = probeAssetDir(dir, isFile);
1289
+ if (found !== undefined) {
1290
+ return found;
1291
+ }
1292
+ dir = parent;
1293
+ parent = dirname(dir);
1294
+ }
1295
+ return probeAssetDir(dir, isFile);
1296
+ }
1297
+ /**
1298
+ * Resolves the directory the compiled debug-UI assets ship in by ascending
1299
+ * from the running module (located via `__dirname` in CommonJS or
1300
+ * `import.meta.url` in ESM). The ascent finds the assets wherever the package
1301
+ * lives — the built dist, an installed `node_modules` copy, or embedded
1302
+ * inside a consumer's bundle output.
1303
+ *
1304
+ * @param isFile - File probe used to confirm the assets exist.
791
1305
  * @returns The absolute debug-UI asset directory.
792
1306
  */
793
- function defaultAssetRoot() {
794
- /* istanbul ignore if -- @preserve the ESM build has no __dirname; the shipped CommonJS build and the test runner always define it */
795
- if (typeof __dirname === 'undefined') {
796
- throw createError('@hyperfrontend/features dev server self-location requires the CommonJS build; inject `assetRoot` otherwise.');
1307
+ function defaultAssetRoot(isFile) {
1308
+ const located = ascendForAssets(currentModuleDir(), isFile);
1309
+ if (located === undefined) {
1310
+ throw 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`.');
797
1311
  }
798
- return join(__dirname, 'debug-ui');
1312
+ return located;
799
1313
  }
800
1314
  /**
801
1315
  * Reads the listening port from a server, throwing if the address is unexpectedly absent.
@@ -906,7 +1420,6 @@ function controlHandler(manifest, assetRoot, deps) {
906
1420
  */
907
1421
  async function startDevServer(config, deps = {}) {
908
1422
  const createServer$1 = deps.createServer ?? createServer;
909
- const assetRoot = deps.assetRoot ?? defaultAssetRoot();
910
1423
  const servers = [];
911
1424
  const apps = [];
912
1425
  for (const app of config.apps) {
@@ -918,6 +1431,8 @@ async function startDevServer(config, deps = {}) {
918
1431
  const manifest = { apps: apps.map((app) => ({ name: app.name, url: app.url })), debug: config.debug };
919
1432
  let debugUrl;
920
1433
  if (config.debug.enabled) {
1434
+ // why: resolved only when the debug UI is enabled so consumers with it disabled never pay for (or fail on) asset self-location.
1435
+ const assetRoot = deps.assetRoot ?? defaultAssetRoot(deps.isFile ?? isFile);
921
1436
  const control = createServer$1(controlHandler(manifest, assetRoot, deps));
922
1437
  const port = await listen(control, config.debugPort);
923
1438
  servers.push(control);
@@ -931,10 +1446,23 @@ async function startDevServer(config, deps = {}) {
931
1446
  };
932
1447
  }
933
1448
 
1449
+ /**
1450
+ * Keeps the dev server running until a termination signal arrives, then closes
1451
+ * every server so the command can exit cleanly.
1452
+ *
1453
+ * @param handle - The running dev server to close once a signal is received.
1454
+ * @returns A promise that resolves after all servers have closed.
1455
+ */
1456
+ async function holdUntilShutdown(handle) {
1457
+ await waitForShutdown();
1458
+ await handle.close();
1459
+ }
934
1460
  /**
935
1461
  * Resolves the `hf-dev.config.*` through the shared tiered loader and starts the
936
- * dev server: one static server per app plus the debug UI. The returned promise
937
- * resolves once the servers are listening; the process stays alive serving them.
1462
+ * dev server: one static server per app plus the debug UI. After printing the
1463
+ * server URLs the returned promise stays pending while the servers run; it
1464
+ * resolves with the success code once a shutdown signal (`SIGINT`/`SIGTERM`,
1465
+ * e.g. Ctrl-C) arrives and every server has closed cleanly.
938
1466
  *
939
1467
  * @param options - Flags, working directory, output sinks, and injectable deps.
940
1468
  * @returns The process exit code.
@@ -949,6 +1477,7 @@ async function runDev(options) {
949
1477
  const cwd = flags.cwd ? resolve(options.cwd, flags.cwd) : options.cwd;
950
1478
  const resolveConfig = options.resolveConfig ?? resolveDevConfig;
951
1479
  const startServer = options.startServer ?? startDevServer;
1480
+ const waitForClose = options.waitForClose ?? holdUntilShutdown;
952
1481
  try {
953
1482
  const config = await resolveConfig({ cwd, flags });
954
1483
  const handle = await startServer(config);
@@ -956,6 +1485,7 @@ async function runDev(options) {
956
1485
  if (handle.debugUrl !== undefined) {
957
1486
  stdout.write(`Debug UI: ${handle.debugUrl}\n`);
958
1487
  }
1488
+ await waitForClose(handle);
959
1489
  return EXIT_OK;
960
1490
  }
961
1491
  catch (error) {
@@ -1131,7 +1661,7 @@ Usage: hf <command> [options]
1131
1661
  Commands:
1132
1662
  init Scaffold the feature glue module and wire it into your app
1133
1663
  build Generate the host connector, bundle it, and pack a tarball
1134
- dev Resolve the dev-server config and start the debug UI
1664
+ dev Start the app servers and debug UI, serving until Ctrl-C
1135
1665
 
1136
1666
  Options:
1137
1667
  --name <name> Feature name
@@ -1140,6 +1670,7 @@ Options:
1140
1670
  --entry <path> Entry file to wire the glue import into (init)
1141
1671
  --url <url> URL the connector loads the feature from (build)
1142
1672
  --protocol <none|v1|v2> Security envelope enforced at build time
1673
+ --allow-open Acknowledge an explicit '--protocol none' and build an open, unauthenticated connector (build)
1143
1674
  --out <dir> Output directory for the built connector (build)
1144
1675
  --apps <path> Path to the dev-server apps array (dev)
1145
1676
  --port <number> Port the dev-server debug UI listens on (dev)