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