@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.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 {
|
|
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
|
|
123
|
-
import { createShell } from '@hyperfrontend/features/host'
|
|
435
|
+
return `import { createShell } from '@hyperfrontend/features/host'
|
|
124
436
|
|
|
125
|
-
|
|
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
|
|
129
|
-
const defaults =
|
|
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:
|
|
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()
|
|
187
|
-
shell.
|
|
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()
|
|
192
|
-
shell.destroy()
|
|
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
|
|
202
|
-
*
|
|
203
|
-
* CLI owns temp-dir
|
|
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,
|
|
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
|
-
|
|
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
|
|
431
|
-
* dir → bundle via the builder → pack a
|
|
432
|
-
*
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
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
|
-
|
|
795
|
-
if (
|
|
796
|
-
throw createError('@hyperfrontend/features dev server
|
|
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
|
|
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.
|
|
937
|
-
*
|
|
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
|
|
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)
|