@mfeflow/contracts 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs.d.ts +1 -0
- package/dist/feature-registry.d.ts +31 -0
- package/dist/feature.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +138 -4
- package/dist/json-schema.d.ts +1 -1
- package/dist/project.d.ts +1 -1
- package/package.json +1 -1
package/dist/docs.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const SCHEMA_URLS: {
|
|
|
7
7
|
readonly feature: "https://mfeflow.github.io/mfeflow/schema/feature.json";
|
|
8
8
|
readonly registryEntry: "https://mfeflow.github.io/mfeflow/schema/registry-entry.json";
|
|
9
9
|
readonly registryIndex: "https://mfeflow.github.io/mfeflow/schema/registry-index.json";
|
|
10
|
+
readonly featureRegistryIndex: "https://mfeflow.github.io/mfeflow/schema/feature-registry-index.json";
|
|
10
11
|
};
|
|
11
12
|
export type SchemaName = keyof typeof SCHEMA_URLS;
|
|
12
13
|
/** The docs page for an error code such as `E102`. */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Issue } from './validate.js';
|
|
2
|
+
/** One feature a third-party registry offers. `name` has no namespace: the registry's own namespace goes in front. */
|
|
3
|
+
export interface FeatureRegistryItem {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
description: string;
|
|
7
|
+
keywords?: string[];
|
|
8
|
+
}
|
|
9
|
+
/** `<base>/index.json` of a third-party feature registry. */
|
|
10
|
+
export interface FeatureRegistryIndex {
|
|
11
|
+
$schema?: string;
|
|
12
|
+
items: FeatureRegistryItem[];
|
|
13
|
+
}
|
|
14
|
+
export declare const REGISTRY_ITEM_NAME: RegExp;
|
|
15
|
+
export declare const MAX_INDEX_ITEMS = 1000;
|
|
16
|
+
export declare const MAX_DESCRIPTION = 300;
|
|
17
|
+
export declare const MAX_KEYWORDS = 20;
|
|
18
|
+
export declare const MAX_KEYWORD = 40;
|
|
19
|
+
/** Plain http is allowed for the machine you are on, so a registry can be tried before it is hosted. */
|
|
20
|
+
export declare function isLoopbackHttp(url: URL): boolean;
|
|
21
|
+
/** True when `url` is https, or http on a loopback host. */
|
|
22
|
+
export declare function isRegistryProtocol(url: URL): boolean;
|
|
23
|
+
/** Why `template` cannot be a registry URL, or undefined when it can. */
|
|
24
|
+
export declare function registryTemplateProblem(template: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Where a registry's `index.json` is: in the folder the template starts in, before the first
|
|
27
|
+
* segment that has a placeholder. `https://x.dev/r/{name}.json` has its index at `https://x.dev/r/index.json`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function registryIndexUrl(template: string): string;
|
|
30
|
+
export declare function validateFeatureRegistryIndex(value: unknown): Issue[];
|
|
31
|
+
export declare function isFeatureRegistryIndex(value: unknown): value is FeatureRegistryIndex;
|
package/dist/feature.d.ts
CHANGED
|
@@ -159,6 +159,8 @@ export interface FeatureManifest {
|
|
|
159
159
|
name: string;
|
|
160
160
|
version: string;
|
|
161
161
|
description?: string;
|
|
162
|
+
/** Words a registry's `index.json` lists for search. */
|
|
163
|
+
keywords?: string[];
|
|
162
164
|
prompts?: FeaturePrompt[];
|
|
163
165
|
requires?: string[];
|
|
164
166
|
conflicts?: string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { ANALYTICS_EVENT, EVENT_NAME, EXPOSE, isMfeManifest, MFE_NAME, ROUTE, va
|
|
|
3
3
|
export { assetsFromManifest, type RemoteAssets } from './manifest-assets.js';
|
|
4
4
|
export { injectIntoHead, registryInjection, type InjectionOptions, type RouteNeed, } from './html-injection.js';
|
|
5
5
|
export { isRegistryEntry, isRegistryIndex, validateRegistryEntry, validateRegistryIndex, type RegistryEntry, type RegistryIndex, type RegistryIndexRemote, } from './registry.js';
|
|
6
|
-
export {
|
|
6
|
+
export { isFeatureRegistryIndex, isLoopbackHttp, isRegistryProtocol, registryIndexUrl, registryTemplateProblem, validateFeatureRegistryIndex, MAX_DESCRIPTION, MAX_INDEX_ITEMS, REGISTRY_ITEM_NAME, type FeatureRegistryIndex, type FeatureRegistryItem, } from './feature-registry.js';
|
|
7
|
+
export { FILE_HASH, REGISTRY_NAMESPACE, isMfeflowProject, validateMfeflowProject, type MfeflowProject, type ProjectBudget, } from './project.js';
|
|
7
8
|
export { COST_MODEL, judgeCost, round1, splitScripts, type CostLine, type CostModel, type CostSample, type CostVerdict, } from './cost-model.js';
|
|
8
9
|
export { FEATURE_CONDITION, FEATURE_NAME, isFeatureManifest, TRANSFORM_OPS, validateFeatureManifest, type AddBootStepOp, type AddEnvKeyOp, type AddImportOp, type AddRouteOp, type AddSharedOp, type AddWorkflowStepOp, type EnvValidator, type FeatureCondition, type FeatureDependency, type FeatureFile, type FeatureManifest, type FeatureMigration, type FeaturePrompt, type FeatureScript, type FeatureTarget, type Literal, type PromptValue, type TransformOp, type TransformOpName, type WorkflowStep, type WrapProviderOp, } from './feature.js';
|
|
9
10
|
export { DOCS_URL, SCHEMA_URLS, errorDocsUrl, type SchemaName } from './docs.js';
|
package/dist/index.js
CHANGED
|
@@ -435,6 +435,99 @@ function isRegistryEntry(value) {
|
|
|
435
435
|
function isRegistryIndex(value) {
|
|
436
436
|
return 0 === validateRegistryIndex(value).length;
|
|
437
437
|
}
|
|
438
|
+
const INDEX_VERSION = /^\d{1,9}\.\d{1,9}\.\d{1,9}(?:-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
|
|
439
|
+
const REGISTRY_ITEM_NAME = /^[a-z][a-z0-9-]*$/;
|
|
440
|
+
const MAX_INDEX_ITEMS = 1000;
|
|
441
|
+
const MAX_DESCRIPTION = 300;
|
|
442
|
+
const MAX_KEYWORDS = 20;
|
|
443
|
+
const MAX_KEYWORD = 40;
|
|
444
|
+
const INVISIBLE = /[\p{Cc}\p{Cf}\p{Cn}\p{Co}\p{Cs}]/u;
|
|
445
|
+
const LOOPBACK_HOSTS = new Set([
|
|
446
|
+
'127.0.0.1',
|
|
447
|
+
'localhost',
|
|
448
|
+
'[::1]'
|
|
449
|
+
]);
|
|
450
|
+
function isLoopbackHttp(url) {
|
|
451
|
+
return 'http:' === url.protocol && LOOPBACK_HOSTS.has(url.hostname);
|
|
452
|
+
}
|
|
453
|
+
function isRegistryProtocol(url) {
|
|
454
|
+
return 'https:' === url.protocol || isLoopbackHttp(url);
|
|
455
|
+
}
|
|
456
|
+
const PLACEHOLDER = /\{([^{}]*)\}/g;
|
|
457
|
+
function registryTemplateProblem(template) {
|
|
458
|
+
if (!template.includes('{name}')) return 'must contain {name}';
|
|
459
|
+
const unknown = [
|
|
460
|
+
...template.matchAll(PLACEHOLDER)
|
|
461
|
+
].map((m)=>m[1] ?? '').find((name)=>'name' !== name && 'version' !== name);
|
|
462
|
+
if (void 0 !== unknown) return `has {${unknown}}; only {name} and {version} are placeholders`;
|
|
463
|
+
if (/[{}]/.test(template.replaceAll(PLACEHOLDER, ''))) return 'has an unmatched { or }';
|
|
464
|
+
const origin = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(template);
|
|
465
|
+
if (origin?.[1]?.includes('{') === true) return 'has a placeholder in the host; put it in the path';
|
|
466
|
+
const first = template.search(/\{(?:name|version)\}/);
|
|
467
|
+
if (null !== origin && template.lastIndexOf('/', first) < origin[0].length) return 'needs a path before the first placeholder, like https://host/{name}.json';
|
|
468
|
+
let url;
|
|
469
|
+
try {
|
|
470
|
+
url = new URL(template.replaceAll('{name}', 'x').replaceAll('{version}', '1'));
|
|
471
|
+
} catch {
|
|
472
|
+
return 'is not a URL';
|
|
473
|
+
}
|
|
474
|
+
if (!isRegistryProtocol(url)) return 'http:' === url.protocol ? 'must be https (http is allowed only for 127.0.0.1, localhost and [::1])' : 'must be an https URL';
|
|
475
|
+
if ('' !== url.username || '' !== url.password) return 'must not contain a user name or password';
|
|
476
|
+
}
|
|
477
|
+
function registryIndexUrl(template) {
|
|
478
|
+
const at = template.search(/\{(?:name|version)\}/);
|
|
479
|
+
const base = template.slice(0, template.lastIndexOf('/', at) + 1);
|
|
480
|
+
return new URL('index.json', base).href;
|
|
481
|
+
}
|
|
482
|
+
function validateFeatureRegistryIndex(value) {
|
|
483
|
+
const c = new Checker();
|
|
484
|
+
if (!isObj(value)) {
|
|
485
|
+
c.add('', 'must be an object');
|
|
486
|
+
return c.issues;
|
|
487
|
+
}
|
|
488
|
+
const items = value.items;
|
|
489
|
+
if (!Array.isArray(items)) {
|
|
490
|
+
c.add('items', 'is required and must be an array');
|
|
491
|
+
return c.issues;
|
|
492
|
+
}
|
|
493
|
+
if (items.length > MAX_INDEX_ITEMS) {
|
|
494
|
+
c.add('items', `has more than ${String(MAX_INDEX_ITEMS)} items`);
|
|
495
|
+
return c.issues;
|
|
496
|
+
}
|
|
497
|
+
const seen = new Set();
|
|
498
|
+
items.forEach((item, i)=>{
|
|
499
|
+
const at = `items[${String(i)}]`;
|
|
500
|
+
if (!isObj(item)) return void c.add(at, 'must be an object');
|
|
501
|
+
const name = c.string(item, 'name', at, {
|
|
502
|
+
pattern: REGISTRY_ITEM_NAME,
|
|
503
|
+
hint: 'must be a feature name without the namespace, like "analytics-segment"'
|
|
504
|
+
});
|
|
505
|
+
if (void 0 !== name) {
|
|
506
|
+
if (seen.has(name)) c.add(`${at}.name`, `"${name}" is listed twice`);
|
|
507
|
+
seen.add(name);
|
|
508
|
+
}
|
|
509
|
+
c.string(item, 'version', at, {
|
|
510
|
+
pattern: INDEX_VERSION,
|
|
511
|
+
hint: 'must be a version like 1.2.0'
|
|
512
|
+
});
|
|
513
|
+
const description = c.string(item, "description", at);
|
|
514
|
+
if (void 0 !== description) {
|
|
515
|
+
if (description.length > MAX_DESCRIPTION) c.add(`${at}.description`, `must be at most ${String(MAX_DESCRIPTION)} characters`);
|
|
516
|
+
else if (INVISIBLE.test(description)) c.add(`${at}.description`, 'must be plain text without control characters');
|
|
517
|
+
}
|
|
518
|
+
const keywords = c.stringArray(item, 'keywords', at, {
|
|
519
|
+
required: false
|
|
520
|
+
});
|
|
521
|
+
if (keywords.length > MAX_KEYWORDS) c.add(`${at}.keywords`, `must have at most ${String(MAX_KEYWORDS)} entries`);
|
|
522
|
+
keywords.forEach((keyword, k)=>{
|
|
523
|
+
if (keyword.length > MAX_KEYWORD || INVISIBLE.test(keyword)) c.add(`${at}.keywords[${String(k)}]`, `must be plain text of at most ${String(MAX_KEYWORD)} characters`);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
return c.issues;
|
|
527
|
+
}
|
|
528
|
+
function isFeatureRegistryIndex(value) {
|
|
529
|
+
return 0 === validateFeatureRegistryIndex(value).length;
|
|
530
|
+
}
|
|
438
531
|
const project_SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\u0000]+`;
|
|
439
532
|
const CHECKOUT_PATH = new RegExp(`^(?:(?![A-Za-z]:)(?:\\.\\.(?:/\\.\\.)*(?:/${project_SEGMENT})*|${project_SEGMENT}(?:/${project_SEGMENT})*)|(?:/|[A-Za-z]:/)${project_SEGMENT}(?:/${project_SEGMENT})*)$`);
|
|
440
533
|
const FILE_HASH = /^sha256-[0-9a-f]{64}$/;
|
|
@@ -519,7 +612,11 @@ function validateMfeflowProject(value) {
|
|
|
519
612
|
const at = `registries.${ns}`;
|
|
520
613
|
if ('@mfeflow' === ns) c.add(at, '@mfeflow is reserved for first-party features');
|
|
521
614
|
else if (!REGISTRY_NAMESPACE.test(ns)) c.add(at, 'namespaces look like "@someone"');
|
|
522
|
-
if ('string' != typeof url
|
|
615
|
+
if ('string' != typeof url) c.add(at, 'must be a URL template like "https://example.com/{name}.json"');
|
|
616
|
+
else {
|
|
617
|
+
const problem = registryTemplateProblem(url);
|
|
618
|
+
if (void 0 !== problem) c.add(at, problem);
|
|
619
|
+
}
|
|
523
620
|
}
|
|
524
621
|
if ('local' in value) {
|
|
525
622
|
const local = c.string(value, 'local', '');
|
|
@@ -789,6 +886,9 @@ function validateFeatureManifest(value) {
|
|
|
789
886
|
c.string(value, "description", '', {
|
|
790
887
|
required: false
|
|
791
888
|
});
|
|
889
|
+
c.stringArray(value, 'keywords', '', {
|
|
890
|
+
required: false
|
|
891
|
+
});
|
|
792
892
|
c.stringArray(value, 'requires', '', {
|
|
793
893
|
required: false,
|
|
794
894
|
pattern: FEATURE_NAME
|
|
@@ -868,7 +968,8 @@ const SCHEMA_URLS = {
|
|
|
868
968
|
project: `${DOCS_URL}/schema/mfeflow.json`,
|
|
869
969
|
feature: `${DOCS_URL}/schema/feature.json`,
|
|
870
970
|
registryEntry: `${DOCS_URL}/schema/registry-entry.json`,
|
|
871
|
-
registryIndex: `${DOCS_URL}/schema/registry-index.json
|
|
971
|
+
registryIndex: `${DOCS_URL}/schema/registry-index.json`,
|
|
972
|
+
featureRegistryIndex: `${DOCS_URL}/schema/feature-registry-index.json`
|
|
872
973
|
};
|
|
873
974
|
function errorDocsUrl(code) {
|
|
874
975
|
return `${DOCS_URL}/errors/${code}`;
|
|
@@ -884,6 +985,7 @@ const json_schema_strings = (pattern)=>({
|
|
|
884
985
|
type: 'array',
|
|
885
986
|
items: str(pattern)
|
|
886
987
|
});
|
|
988
|
+
const REGISTRY_URL = /^(?:https:\/\/|http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?:[:/?#])).*\{name\}/;
|
|
887
989
|
const literal = {
|
|
888
990
|
type: [
|
|
889
991
|
'string',
|
|
@@ -1228,7 +1330,7 @@ const JSON_SCHEMAS = {
|
|
|
1228
1330
|
},
|
|
1229
1331
|
additionalProperties: {
|
|
1230
1332
|
type: 'string',
|
|
1231
|
-
pattern:
|
|
1333
|
+
pattern: REGISTRY_URL.source
|
|
1232
1334
|
}
|
|
1233
1335
|
},
|
|
1234
1336
|
local: str(CHECKOUT_PATH),
|
|
@@ -1262,6 +1364,7 @@ const JSON_SCHEMAS = {
|
|
|
1262
1364
|
name: str(FEATURE_NAME),
|
|
1263
1365
|
version: str(),
|
|
1264
1366
|
description: str(),
|
|
1367
|
+
keywords: json_schema_strings(),
|
|
1265
1368
|
prompts: {
|
|
1266
1369
|
type: 'array',
|
|
1267
1370
|
items: json_schema_item({
|
|
@@ -1418,6 +1521,37 @@ const JSON_SCHEMAS = {
|
|
|
1418
1521
|
required: [
|
|
1419
1522
|
'remotes'
|
|
1420
1523
|
]
|
|
1524
|
+
}),
|
|
1525
|
+
featureRegistryIndex: root('featureRegistryIndex', 'feature registry index', {
|
|
1526
|
+
properties: {
|
|
1527
|
+
items: {
|
|
1528
|
+
type: 'array',
|
|
1529
|
+
maxItems: 1000,
|
|
1530
|
+
items: json_schema_item({
|
|
1531
|
+
name: str(REGISTRY_ITEM_NAME),
|
|
1532
|
+
version: str(),
|
|
1533
|
+
description: {
|
|
1534
|
+
type: 'string',
|
|
1535
|
+
maxLength: 300
|
|
1536
|
+
},
|
|
1537
|
+
keywords: {
|
|
1538
|
+
type: 'array',
|
|
1539
|
+
maxItems: 20,
|
|
1540
|
+
items: {
|
|
1541
|
+
type: 'string',
|
|
1542
|
+
maxLength: 40
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}, [
|
|
1546
|
+
'name',
|
|
1547
|
+
'version',
|
|
1548
|
+
"description"
|
|
1549
|
+
])
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
required: [
|
|
1553
|
+
'items'
|
|
1554
|
+
]
|
|
1421
1555
|
})
|
|
1422
1556
|
};
|
|
1423
|
-
export { ANALYTICS_EVENT, COST_MODEL, DOCS_URL, EVENT_NAME, EXPOSE, FEATURE_CONDITION, FEATURE_NAME, FILE_HASH, JSON_SCHEMAS, MFE_NAME, ROUTE, SCHEMA_URLS, TRANSFORM_OPS, assetsFromManifest, errorDocsUrl, injectIntoHead, isFeatureManifest, isMfeManifest, isMfeflowProject, isRegistryEntry, isRegistryIndex, isSafeRelativePath, judgeCost, registryInjection, round1, splitScripts, validateFeatureManifest, validateMfeManifest, validateMfeflowProject, validateRegistryEntry, validateRegistryIndex };
|
|
1557
|
+
export { ANALYTICS_EVENT, COST_MODEL, DOCS_URL, EVENT_NAME, EXPOSE, FEATURE_CONDITION, FEATURE_NAME, FILE_HASH, JSON_SCHEMAS, MAX_DESCRIPTION, MAX_INDEX_ITEMS, MFE_NAME, REGISTRY_ITEM_NAME, REGISTRY_NAMESPACE, ROUTE, SCHEMA_URLS, TRANSFORM_OPS, assetsFromManifest, errorDocsUrl, injectIntoHead, isFeatureManifest, isFeatureRegistryIndex, isLoopbackHttp, isMfeManifest, isMfeflowProject, isRegistryEntry, isRegistryIndex, isRegistryProtocol, isSafeRelativePath, judgeCost, registryIndexUrl, registryInjection, registryTemplateProblem, round1, splitScripts, validateFeatureManifest, validateFeatureRegistryIndex, validateMfeManifest, validateMfeflowProject, validateRegistryEntry, validateRegistryIndex };
|
package/dist/json-schema.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SchemaName } from './docs.js';
|
|
2
2
|
type Json = Record<string, unknown>;
|
|
3
3
|
/**
|
|
4
|
-
* Editor schemas for the
|
|
4
|
+
* Editor schemas for the six JSON documents mfeflow reads. They mirror the validators in this package; the
|
|
5
5
|
* validators stay the authority (they also check things a schema cannot, like duplicate routes), so a document
|
|
6
6
|
* can pass here and still fail `mfeflow validate`.
|
|
7
7
|
*/
|
package/dist/project.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface MfeflowProject {
|
|
|
18
18
|
}>;
|
|
19
19
|
/** Hashes of copied files, so `upgrade` can tell when a file was edited. */
|
|
20
20
|
files?: Record<string, string>;
|
|
21
|
-
/** Third-party feature registries: `{ "@someone": "https://…/{name}.json" }
|
|
21
|
+
/** Third-party feature registries: `{ "@someone": "https://…/{name}.json" }`, https only (plain http for loopback hosts). `@mfeflow` is reserved. */
|
|
22
22
|
registries?: Record<string, string>;
|
|
23
23
|
/** Built mfeflow checkout, relative to the project root, that `@mfeflow/*` dependencies link to. Set by `--local` only. */
|
|
24
24
|
local?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Types, validators and JSON Schemas for mfeflow: mfe.json ownership manifests, mfeflow.json project state, and the remote registry (entries and index).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|