@mfeflow/contracts 0.4.0 → 0.6.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/README.md +1 -1
- package/dist/docs.d.ts +7 -1
- package/dist/entry.d.ts +45 -0
- package/dist/entry.js +139 -2
- package/dist/feature-registry.d.ts +15 -0
- package/dist/html-injection.d.ts +2 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +328 -109
- package/dist/mfe-manifest.d.ts +3 -3
- package/dist/project.d.ts +23 -0
- package/dist/registry.d.ts +4 -23
- package/dist/remote-budget.d.ts +21 -0
- package/dist/remote-dist.d.ts +12 -0
- package/dist/routes.d.ts +6 -0
- package/dist/validate.d.ts +8 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Part of [mfeflow](https://github.com/mfeflow/mfeflow), the React micro-frontend boilerplate and platform kit.
|
|
4
4
|
|
|
5
|
-
Types, validators and JSON Schemas for `
|
|
5
|
+
Types, validators and JSON Schemas for `mfeflow.app.json`, `mfeflow.json` and the remote registry. Pre-1.0: minor versions may change the API.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
pnpm add @mfeflow/contracts
|
package/dist/docs.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export declare const DOCS_URL = "https://mfeflow.github.io/mfeflow";
|
|
3
3
|
/** Where the JSON Schemas are published, for `$schema` in editors. */
|
|
4
4
|
export declare const SCHEMA_URLS: {
|
|
5
|
-
|
|
5
|
+
/** `mfeflow.app.json`, the file in each app's folder. */
|
|
6
|
+
readonly mfe: "https://mfeflow.github.io/mfeflow/schema/mfeflow-app.json";
|
|
6
7
|
readonly project: "https://mfeflow.github.io/mfeflow/schema/mfeflow.json";
|
|
7
8
|
readonly feature: "https://mfeflow.github.io/mfeflow/schema/feature.json";
|
|
8
9
|
readonly registryEntry: "https://mfeflow.github.io/mfeflow/schema/registry-entry.json";
|
|
@@ -10,5 +11,10 @@ export declare const SCHEMA_URLS: {
|
|
|
10
11
|
readonly featureRegistryIndex: "https://mfeflow.github.io/mfeflow/schema/feature-registry-index.json";
|
|
11
12
|
};
|
|
12
13
|
export type SchemaName = keyof typeof SCHEMA_URLS;
|
|
14
|
+
/**
|
|
15
|
+
* Old schema URLs that still serve a schema, so `$schema` lines written before a rename keep
|
|
16
|
+
* resolving. Each maps to the schema now published at its `SCHEMA_URLS` address.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SCHEMA_ALIASES: Readonly<Record<string, SchemaName>>;
|
|
13
19
|
/** The docs page for an error code such as `E102`. */
|
|
14
20
|
export declare function errorDocsUrl(code: string): string;
|
package/dist/entry.d.ts
CHANGED
|
@@ -5,6 +5,51 @@ export declare const ENTRY_TYPES: readonly string[];
|
|
|
5
5
|
* also hold `-`, as the `globalName` in `mf-manifest.json` does for a remote like `my-app`.
|
|
6
6
|
*/
|
|
7
7
|
export declare const GLOBAL_NAME: RegExp;
|
|
8
|
+
/** A Module Federation name that is also safe as a plain object key. */
|
|
9
|
+
export declare const isRemoteName: (value: string) => boolean;
|
|
10
|
+
export declare const ROUTE: RegExp;
|
|
11
|
+
export declare const EXPOSE: RegExp;
|
|
12
|
+
/** One remote of the registry index, the shape the shell, the edge, the CLI and the dev registry share. */
|
|
13
|
+
export interface RegistryIndexRemote {
|
|
14
|
+
/** Module Federation name. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** `mf-manifest.json` URL. */
|
|
17
|
+
entry: string;
|
|
18
|
+
/** Entry script URL; shells register it in place of `entry` when present. */
|
|
19
|
+
remoteEntry?: string;
|
|
20
|
+
/** How `remoteEntry` loads (`module` for ESM). */
|
|
21
|
+
type?: string;
|
|
22
|
+
/** The global `remoteEntry` defines when it differs from `name`. */
|
|
23
|
+
entryGlobalName?: string;
|
|
24
|
+
/** Per expose (`./App`), the files to preload. Not inlined into the shell's HTML. */
|
|
25
|
+
exposes?: Record<string, string[]>;
|
|
26
|
+
version?: string;
|
|
27
|
+
routes?: string[];
|
|
28
|
+
owner?: string;
|
|
29
|
+
/** The version served before `version`; what a rollback returns to. */
|
|
30
|
+
previous?: string;
|
|
31
|
+
/** Older versions served, newest first and bounded. */
|
|
32
|
+
history?: string[];
|
|
33
|
+
/** Set after a rollback: the version that was served and was moved away from. Never a rollback target. */
|
|
34
|
+
rolledBackFrom?: string;
|
|
35
|
+
}
|
|
36
|
+
/** What the shell fetches at boot. */
|
|
37
|
+
export interface RegistryIndex {
|
|
38
|
+
shell?: string;
|
|
39
|
+
env?: string;
|
|
40
|
+
generatedAt?: string;
|
|
41
|
+
remotes: RegistryIndexRemote[];
|
|
42
|
+
}
|
|
43
|
+
export interface RegistryIssue {
|
|
44
|
+
/** Relative to the value checked, e.g. `type` or `exposes./App[0]`. */
|
|
45
|
+
path: string;
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
/** Problems with the entry script fields a registry entry and an index remote share. */
|
|
49
|
+
export declare function remoteFileIssues(obj: Record<string, unknown>): RegistryIssue[];
|
|
50
|
+
/** Every problem with one registry index remote; empty when it is valid. Browser-safe: the shell runs it. */
|
|
51
|
+
export declare function registryRemoteIssues(value: unknown): RegistryIssue[];
|
|
52
|
+
export declare function isRegistryIndexRemote(value: unknown): value is RegistryIndexRemote;
|
|
8
53
|
/** The registry remote fields the shell and the edge read; everything else is dropped. */
|
|
9
54
|
export declare const REGISTRY_REMOTE_FIELDS: readonly string[];
|
|
10
55
|
/** A copy of `remote` with only the whitelisted fields that are set. */
|
package/dist/entry.js
CHANGED
|
@@ -20,19 +20,156 @@ const ENTRY_TYPES = [
|
|
|
20
20
|
'system'
|
|
21
21
|
];
|
|
22
22
|
const GLOBAL_NAME = /^[A-Za-z_$][\w$-]*$/;
|
|
23
|
+
const RESERVED_NAMES = new Set([
|
|
24
|
+
'__proto__',
|
|
25
|
+
'constructor',
|
|
26
|
+
'prototype'
|
|
27
|
+
]);
|
|
28
|
+
const isRemoteName = (value)=>GLOBAL_NAME.test(value) && !RESERVED_NAMES.has(value);
|
|
29
|
+
const ROUTE = /^\/[A-Za-z0-9/_:*.-]*$/;
|
|
30
|
+
const EXPOSE = /^\.\/[A-Za-z0-9_/-]+$/;
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return 'object' == typeof value && null !== value && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isHttpUrl(value) {
|
|
35
|
+
try {
|
|
36
|
+
const { protocol } = new URL(value);
|
|
37
|
+
return 'https:' === protocol || 'http:' === protocol;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function stringField(obj, key, out, check, required = false) {
|
|
43
|
+
const value = obj[key];
|
|
44
|
+
if (void 0 === value) {
|
|
45
|
+
if (required) out.push({
|
|
46
|
+
path: key,
|
|
47
|
+
message: 'is required'
|
|
48
|
+
});
|
|
49
|
+
} else if ('string' != typeof value) out.push({
|
|
50
|
+
path: key,
|
|
51
|
+
message: 'must be a string'
|
|
52
|
+
});
|
|
53
|
+
else if (check && !check.test(value)) out.push({
|
|
54
|
+
path: key,
|
|
55
|
+
message: check.hint
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const URL_CHECK = {
|
|
59
|
+
test: isHttpUrl,
|
|
60
|
+
hint: 'must be an http(s) URL'
|
|
61
|
+
};
|
|
62
|
+
function remoteFileIssues(obj) {
|
|
63
|
+
const out = [];
|
|
64
|
+
stringField(obj, 'remoteEntry', out, URL_CHECK);
|
|
65
|
+
stringField(obj, 'type', out, {
|
|
66
|
+
test: (value)=>ENTRY_TYPES.includes(value),
|
|
67
|
+
hint: `must be a Module Federation entry type: ${ENTRY_TYPES.join(', ')}`
|
|
68
|
+
});
|
|
69
|
+
stringField(obj, 'entryGlobalName', out, {
|
|
70
|
+
test: (value)=>GLOBAL_NAME.test(value),
|
|
71
|
+
hint: 'must be a JavaScript global name (letters, digits, _, $, -; not starting with a digit)'
|
|
72
|
+
});
|
|
73
|
+
const exposes = obj.exposes;
|
|
74
|
+
if (void 0 === exposes) return out;
|
|
75
|
+
if (!isRecord(exposes)) return [
|
|
76
|
+
...out,
|
|
77
|
+
{
|
|
78
|
+
path: 'exposes',
|
|
79
|
+
message: 'must be an object'
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
for (const [expose, files] of Object.entries(exposes)){
|
|
83
|
+
const at = `exposes.${expose}`;
|
|
84
|
+
if (!EXPOSE.test(expose)) out.push({
|
|
85
|
+
path: at,
|
|
86
|
+
message: 'must look like "./App"'
|
|
87
|
+
});
|
|
88
|
+
if (!Array.isArray(files)) {
|
|
89
|
+
out.push({
|
|
90
|
+
path: at,
|
|
91
|
+
message: 'must be an array of URLs'
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
files.forEach((file, i)=>{
|
|
96
|
+
if ('string' != typeof file || !isHttpUrl(file)) out.push({
|
|
97
|
+
path: `${at}[${String(i)}]`,
|
|
98
|
+
message: 'must be an http(s) URL'
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function registryRemoteIssues(value) {
|
|
105
|
+
if (!isRecord(value)) return [
|
|
106
|
+
{
|
|
107
|
+
path: '',
|
|
108
|
+
message: 'must be an object'
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
const out = [];
|
|
112
|
+
stringField(value, 'name', out, {
|
|
113
|
+
test: isRemoteName,
|
|
114
|
+
hint: 'must be a Module Federation name'
|
|
115
|
+
}, true);
|
|
116
|
+
stringField(value, 'entry', out, URL_CHECK, true);
|
|
117
|
+
stringField(value, 'version', out);
|
|
118
|
+
stringField(value, 'owner', out);
|
|
119
|
+
stringField(value, 'previous', out);
|
|
120
|
+
stringField(value, 'rolledBackFrom', out);
|
|
121
|
+
const history = value.history;
|
|
122
|
+
if (void 0 !== history) if (Array.isArray(history)) history.forEach((version, i)=>{
|
|
123
|
+
if ('string' != typeof version) out.push({
|
|
124
|
+
path: `history[${String(i)}]`,
|
|
125
|
+
message: 'must be a string'
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
else out.push({
|
|
129
|
+
path: 'history',
|
|
130
|
+
message: 'must be an array of strings'
|
|
131
|
+
});
|
|
132
|
+
const routes = value.routes;
|
|
133
|
+
if (void 0 !== routes) if (Array.isArray(routes)) routes.forEach((route, i)=>{
|
|
134
|
+
const at = `routes[${String(i)}]`;
|
|
135
|
+
if ('string' != typeof route) out.push({
|
|
136
|
+
path: at,
|
|
137
|
+
message: 'must be a string'
|
|
138
|
+
});
|
|
139
|
+
else if (!ROUTE.test(route)) out.push({
|
|
140
|
+
path: at,
|
|
141
|
+
message: 'must start with "/"'
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
else out.push({
|
|
145
|
+
path: 'routes',
|
|
146
|
+
message: 'must be an array of strings'
|
|
147
|
+
});
|
|
148
|
+
return [
|
|
149
|
+
...out,
|
|
150
|
+
...remoteFileIssues(value)
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
function isRegistryIndexRemote(value) {
|
|
154
|
+
return 0 === registryRemoteIssues(value).length;
|
|
155
|
+
}
|
|
23
156
|
const REGISTRY_REMOTE_FIELDS = [
|
|
24
157
|
'name',
|
|
25
158
|
'entry',
|
|
26
159
|
'remoteEntry',
|
|
27
160
|
'type',
|
|
28
161
|
'entryGlobalName',
|
|
162
|
+
'exposes',
|
|
29
163
|
'version',
|
|
30
164
|
'owner',
|
|
31
|
-
'routes'
|
|
165
|
+
'routes',
|
|
166
|
+
'previous',
|
|
167
|
+
'history',
|
|
168
|
+
'rolledBackFrom'
|
|
32
169
|
];
|
|
33
170
|
function pickRegistryFields(remote) {
|
|
34
171
|
const out = {};
|
|
35
172
|
for (const [key, value] of Object.entries(remote))if (void 0 !== value && REGISTRY_REMOTE_FIELDS.includes(key)) out[key] = value;
|
|
36
173
|
return out;
|
|
37
174
|
}
|
|
38
|
-
export { ENTRY_TYPES, GLOBAL_NAME, REGISTRY_REMOTE_FIELDS, pickRegistryFields };
|
|
175
|
+
export { ENTRY_TYPES, EXPOSE, GLOBAL_NAME, REGISTRY_REMOTE_FIELDS, ROUTE, isRegistryIndexRemote, isRemoteName, pickRegistryFields, registryRemoteIssues, remoteFileIssues };
|
|
@@ -29,3 +29,18 @@ export declare function registryTemplateProblem(template: string): string | unde
|
|
|
29
29
|
export declare function registryIndexUrl(template: string): string;
|
|
30
30
|
export declare function validateFeatureRegistryIndex(value: unknown): Issue[];
|
|
31
31
|
export declare function isFeatureRegistryIndex(value: unknown): value is FeatureRegistryIndex;
|
|
32
|
+
/**
|
|
33
|
+
* True when `hostname` (as `URL.hostname` spells it) is the machine itself or a private network:
|
|
34
|
+
* localhost, 127/8, ::1, 10/8, 172.16/12, 192.168/16, 169.254/16, 100.64/10, 192.0.0/24, 0/8, fc00::/7,
|
|
35
|
+
* fe80::/10, fec0::/10, IPv4-compatible, IPv4-mapped, NAT64 and 6to4 IPv6 forms that hold one of
|
|
36
|
+
* those, and names ending .localhost, .internal or .local.
|
|
37
|
+
* Only the literal host is looked at; a public name that resolves to a private address is not caught.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isPrivateHost(hostname: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* `url` with the values of token-like query parameters (token, key, sig, auth, password, secret,
|
|
42
|
+
* X-Amz-*) and any user name or password replaced by `***`. Works on templates too, so `{name}` stays.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactUrl(url: string): string;
|
|
45
|
+
/** `text` with every http(s) URL in it passed through `redactUrl`, for messages that quote one. */
|
|
46
|
+
export declare function redactUrlsIn(text: string): string;
|
package/dist/html-injection.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export interface InjectionOptions {
|
|
|
18
18
|
* index) and preload hints for the files each remote the route renders needs.
|
|
19
19
|
* Everything the runtime reads from the fetched index is inlined, so both
|
|
20
20
|
* paths register the same remotes; only the per-expose `exposes` file lists
|
|
21
|
-
*
|
|
21
|
+
* and the rollback fields (`previous`, `history`, `rolledBackFrom`) stay out
|
|
22
|
+
* of the page.
|
|
22
23
|
*
|
|
23
24
|
* Hints match how the browser will request each file, or it downloads twice:
|
|
24
25
|
* classic scripts and styles are `rel="preload"` without `crossorigin`; the
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export { isSafeRelativePath, type Issue } from './validate.js';
|
|
1
|
+
export { isPortableRelativePath, isSafeRelativePath, type Issue } from './validate.js';
|
|
2
2
|
export { ANALYTICS_EVENT, EVENT_NAME, EXPOSE, isMfeManifest, MFE_NAME, ROUTE, validateMfeManifest, type MfeManifest, } from './mfe-manifest.js';
|
|
3
|
+
export { routesOverlap } from './routes.js';
|
|
3
4
|
export { assetsFromManifest, type RemoteAssets } from './manifest-assets.js';
|
|
5
|
+
export { remoteDistFiles, type RemoteDistFiles } from './remote-dist.js';
|
|
6
|
+
export { judgeRemoteInitial, primaryExpose, type RemoteInitialVerdict } from './remote-budget.js';
|
|
4
7
|
export { injectIntoHead, registryInjection, type InjectionOptions, type RouteNeed, } from './html-injection.js';
|
|
5
|
-
export { isRegistryEntry, isRegistryIndex, validateRegistryEntry, validateRegistryIndex, type RegistryEntry, type RegistryIndex, type RegistryIndexRemote, } from './registry.js';
|
|
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';
|
|
8
|
+
export { isRegistryEntry, isRegistryIndex, isRegistryIndexRemote, registryRemoteIssues, ROUTE_OVERLAP, validateRegistryEntry, validateRegistryIndex, type RegistryEntry, type RegistryIndex, type RegistryIndexRemote, } from './registry.js';
|
|
9
|
+
export { isFeatureRegistryIndex, isLoopbackHttp, isPrivateHost, isRegistryProtocol, redactUrl, redactUrlsIn, registryIndexUrl, registryTemplateProblem, validateFeatureRegistryIndex, MAX_DESCRIPTION, MAX_INDEX_ITEMS, REGISTRY_ITEM_NAME, type FeatureRegistryIndex, type FeatureRegistryItem, } from './feature-registry.js';
|
|
10
|
+
export { FILE_HASH, PROJECT_REPOS, REGISTRY_HOST, REGISTRY_NAMESPACE, isMfeflowProject, validateMfeflowProject, type MfeflowProject, type ProjectBudget, type ProjectRegistry, } from './project.js';
|
|
8
11
|
export { COST_MODEL, judgeCost, round1, splitScripts, type CostLine, type CostModel, type CostSample, type CostVerdict, } from './cost-model.js';
|
|
9
12
|
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';
|
|
10
|
-
export { DOCS_URL, SCHEMA_URLS, errorDocsUrl, type SchemaName } from './docs.js';
|
|
13
|
+
export { DOCS_URL, SCHEMA_ALIASES, SCHEMA_URLS, errorDocsUrl, type SchemaName } from './docs.js';
|
|
11
14
|
export { JSON_SCHEMAS } from './json-schema.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registryRemoteIssues, GLOBAL_NAME, remoteFileIssues, ENTRY_TYPES, pickRegistryFields, EXPOSE, isRemoteName, ROUTE } from "./entry.js";
|
|
2
2
|
function isObj(value) {
|
|
3
3
|
return 'object' == typeof value && null !== value && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
class Checker {
|
|
6
6
|
issues = [];
|
|
7
|
-
add(path, message) {
|
|
8
|
-
this.issues.push({
|
|
7
|
+
add(path, message, code) {
|
|
8
|
+
this.issues.push(void 0 === code ? {
|
|
9
9
|
path,
|
|
10
10
|
message
|
|
11
|
+
} : {
|
|
12
|
+
path,
|
|
13
|
+
message,
|
|
14
|
+
code
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
string(obj, key, path, opts = {}) {
|
|
@@ -78,9 +82,12 @@ function isSafeRelativePath(value) {
|
|
|
78
82
|
if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) return false;
|
|
79
83
|
return value.split('/').every((part)=>'' !== part && '.' !== part && '..' !== part);
|
|
80
84
|
}
|
|
85
|
+
const DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
86
|
+
function isPortableRelativePath(value) {
|
|
87
|
+
if (!isSafeRelativePath(value)) return false;
|
|
88
|
+
return value.split('/').every((part)=>!part.includes(':') && !DEVICE_NAME.test(part) && !/[. ]$/.test(part));
|
|
89
|
+
}
|
|
81
90
|
const MFE_NAME = /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/;
|
|
82
|
-
const ROUTE = /^\/[A-Za-z0-9/_:*.-]*$/;
|
|
83
|
-
const EXPOSE = /^\.\/[A-Za-z0-9_/-]+$/;
|
|
84
91
|
const EVENT_NAME = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+\.v[1-9][0-9]*$/;
|
|
85
92
|
const ANALYTICS_EVENT = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
86
93
|
function validateMfeManifest(value) {
|
|
@@ -146,6 +153,32 @@ function validateMfeManifest(value) {
|
|
|
146
153
|
function isMfeManifest(value) {
|
|
147
154
|
return 0 === validateMfeManifest(value).length;
|
|
148
155
|
}
|
|
156
|
+
function segments(route) {
|
|
157
|
+
return route.toLowerCase().split('/').filter(Boolean);
|
|
158
|
+
}
|
|
159
|
+
const isParam = (segment)=>segment.startsWith(':');
|
|
160
|
+
const isWild = (segment)=>segment.endsWith('*');
|
|
161
|
+
const prefixOf = (segment)=>isParam(segment) ? '' : segment.slice(0, -1);
|
|
162
|
+
const tailAt = (list, i)=>i === list.length - 1 && isWild(list[i] ?? '');
|
|
163
|
+
function segmentsMeet(x, y) {
|
|
164
|
+
if (isWild(x) && isWild(y)) return x.startsWith(prefixOf(y)) || y.startsWith(prefixOf(x));
|
|
165
|
+
if (isWild(x)) return isParam(y) || y.startsWith(prefixOf(x));
|
|
166
|
+
if (isWild(y)) return isParam(x) || x.startsWith(prefixOf(y));
|
|
167
|
+
return x === y || isParam(x) || isParam(y);
|
|
168
|
+
}
|
|
169
|
+
function routesOverlap(a, b) {
|
|
170
|
+
const left = segments(a);
|
|
171
|
+
const right = segments(b);
|
|
172
|
+
for(let i = 0; i < Math.max(left.length, right.length); i += 1){
|
|
173
|
+
const x = left[i];
|
|
174
|
+
const y = right[i];
|
|
175
|
+
if (void 0 === x) return tailAt(right, i) && '' === prefixOf(y ?? '');
|
|
176
|
+
if (void 0 === y) return tailAt(left, i) && '' === prefixOf(x);
|
|
177
|
+
if (!segmentsMeet(x, y)) return false;
|
|
178
|
+
if (tailAt(left, i) || tailAt(right, i)) break;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
149
182
|
function fail(why) {
|
|
150
183
|
throw new Error(`mf-manifest.json: ${why}`);
|
|
151
184
|
}
|
|
@@ -226,6 +259,107 @@ function assetsFromManifest(manifest, manifestUrl, options = {}) {
|
|
|
226
259
|
}
|
|
227
260
|
};
|
|
228
261
|
}
|
|
262
|
+
const DIST_ORIGIN = 'https://dist.invalid';
|
|
263
|
+
const SCRIPT = /\.[cm]?js$/;
|
|
264
|
+
function distPath(url) {
|
|
265
|
+
try {
|
|
266
|
+
return decodeURIComponent(new URL(url).pathname).replace(/^\//, '');
|
|
267
|
+
} catch {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function remoteDistFiles(manifest) {
|
|
272
|
+
if (!isObj(manifest) || !isObj(manifest.metaData) || !isObj(manifest.metaData.remoteEntry)) return;
|
|
273
|
+
const local = {
|
|
274
|
+
...manifest,
|
|
275
|
+
metaData: {
|
|
276
|
+
...manifest.metaData,
|
|
277
|
+
publicPath: 'auto',
|
|
278
|
+
getPublicPath: void 0,
|
|
279
|
+
globalName: void 0,
|
|
280
|
+
remoteEntry: {
|
|
281
|
+
...manifest.metaData.remoteEntry,
|
|
282
|
+
type: void 0
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
let assets;
|
|
287
|
+
try {
|
|
288
|
+
assets = assetsFromManifest(local, `${DIST_ORIGIN}/mf-manifest.json`);
|
|
289
|
+
} catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const entry = void 0 === assets.remoteEntry ? void 0 : distPath(assets.remoteEntry);
|
|
293
|
+
if (void 0 === entry) return;
|
|
294
|
+
const exposes = {};
|
|
295
|
+
for (const [name, urls] of Object.entries(assets.exposes))exposes[name] = urls.flatMap((url)=>distPath(url) ?? []).filter((f)=>SCRIPT.test(f));
|
|
296
|
+
return {
|
|
297
|
+
entry,
|
|
298
|
+
exposes
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const COST_MODEL = {
|
|
302
|
+
lcpDeltaMs: 300,
|
|
303
|
+
shellKb: 32,
|
|
304
|
+
remoteKb: 10
|
|
305
|
+
};
|
|
306
|
+
const KB = 1024;
|
|
307
|
+
const round1 = (value)=>Math.round(10 * value) / 10;
|
|
308
|
+
const line = (actual, budget)=>({
|
|
309
|
+
budget,
|
|
310
|
+
actual,
|
|
311
|
+
pass: actual <= budget
|
|
312
|
+
});
|
|
313
|
+
function judgeCost(sample, model = COST_MODEL) {
|
|
314
|
+
const lcp = line(Math.round(sample.lcpDeltaMs), model.lcpDeltaMs);
|
|
315
|
+
const shell = line(round1((sample.shellBytes - sample.monolithBytes) / KB), model.shellKb);
|
|
316
|
+
const remotes = Object.entries(sample.remoteBytes).toSorted(([a], [b])=>a < b ? -1 : a > b ? 1 : 0).map(([name, bytes])=>Object.assign(line(round1(bytes / KB), model.remoteKb), {
|
|
317
|
+
name
|
|
318
|
+
}));
|
|
319
|
+
return {
|
|
320
|
+
pass: lcp.pass && shell.pass && remotes.every((r)=>r.pass),
|
|
321
|
+
lcp,
|
|
322
|
+
shell,
|
|
323
|
+
remotes
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const REMOTE_PATH = /^\/mfe\/([^/]+)\//;
|
|
327
|
+
function splitScripts(requests) {
|
|
328
|
+
let shellBytes = 0;
|
|
329
|
+
const remoteBytes = {};
|
|
330
|
+
for (const request of requests){
|
|
331
|
+
if ('Script' !== request.type) continue;
|
|
332
|
+
const remote = REMOTE_PATH.exec(request.path)?.[1];
|
|
333
|
+
if (void 0 === remote) shellBytes += request.bytes;
|
|
334
|
+
else remoteBytes[remote] = (remoteBytes[remote] ?? 0) + request.bytes;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
shellBytes,
|
|
338
|
+
remoteBytes
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const remote_budget_KB = 1024;
|
|
342
|
+
function primaryExpose(names) {
|
|
343
|
+
return names.includes('./App') ? './App' : names[0];
|
|
344
|
+
}
|
|
345
|
+
function judgeRemoteInitial(bytes, budgetKb, model = COST_MODEL) {
|
|
346
|
+
const totalKb = round1((bytes.entryBytes + bytes.exposeBytes) / remote_budget_KB);
|
|
347
|
+
if (void 0 !== budgetKb) return {
|
|
348
|
+
kb: totalKb,
|
|
349
|
+
totalKb,
|
|
350
|
+
limitKb: budgetKb,
|
|
351
|
+
pass: totalKb <= budgetKb,
|
|
352
|
+
source: 'mfeflow.app.json'
|
|
353
|
+
};
|
|
354
|
+
const kb = round1(bytes.entryBytes / remote_budget_KB);
|
|
355
|
+
return {
|
|
356
|
+
kb,
|
|
357
|
+
totalKb,
|
|
358
|
+
limitKb: model.remoteKb,
|
|
359
|
+
pass: kb <= model.remoteKb,
|
|
360
|
+
source: 'cost-model'
|
|
361
|
+
};
|
|
362
|
+
}
|
|
229
363
|
const SCRIPT_UNSAFE = new RegExp('[<\\u2028\\u2029]', 'g');
|
|
230
364
|
function jsonForScript(value) {
|
|
231
365
|
return JSON.stringify(value).replace(SCRIPT_UNSAFE, (c)=>`\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
@@ -237,9 +371,15 @@ const ESM_TYPES = new Set([
|
|
|
237
371
|
'module',
|
|
238
372
|
'esm'
|
|
239
373
|
]);
|
|
374
|
+
const NOT_INLINED = new Set([
|
|
375
|
+
'exposes',
|
|
376
|
+
'previous',
|
|
377
|
+
'history',
|
|
378
|
+
'rolledBackFrom'
|
|
379
|
+
]);
|
|
240
380
|
function registryInjection(index, needs, options = {}) {
|
|
241
381
|
const inline = {
|
|
242
|
-
remotes: index.remotes.map((remote)=>pickRegistryFields(remote))
|
|
382
|
+
remotes: index.remotes.map((remote)=>Object.fromEntries(Object.entries(pickRegistryFields(remote)).filter(([key])=>!NOT_INLINED.has(key))))
|
|
243
383
|
};
|
|
244
384
|
let html = `<script>window.__MFE_REGISTRY__=${jsonForScript(inline)}</script>`;
|
|
245
385
|
const seen = new Set();
|
|
@@ -307,36 +447,13 @@ function injectIntoHead(html, snippet) {
|
|
|
307
447
|
const end = -1 === charset ? open : charset;
|
|
308
448
|
return html.slice(0, end) + snippet + html.slice(end);
|
|
309
449
|
}
|
|
310
|
-
const
|
|
450
|
+
const ROUTE_OVERLAP = 'route-overlap';
|
|
311
451
|
const VERSION = /^[0-9A-Za-z.+-]+$/;
|
|
312
|
-
function
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
c.string(obj, 'type', at, {
|
|
318
|
-
required: false,
|
|
319
|
-
pattern: ENTRY_TYPE,
|
|
320
|
-
hint: `must be a Module Federation entry type: ${ENTRY_TYPES.join(', ')}`
|
|
321
|
-
});
|
|
322
|
-
c.string(obj, 'entryGlobalName', at, {
|
|
323
|
-
required: false,
|
|
324
|
-
pattern: GLOBAL_NAME,
|
|
325
|
-
hint: 'must be a JavaScript global name (letters, digits, _, $, -; not starting with a digit)'
|
|
326
|
-
});
|
|
327
|
-
const exposes = c.object(obj, 'exposes', at, false);
|
|
328
|
-
if (!exposes) return;
|
|
329
|
-
const base = at ? `${at}.exposes` : 'exposes';
|
|
330
|
-
for (const [expose, files] of Object.entries(exposes)){
|
|
331
|
-
if (!EXPOSE.test(expose)) c.add(`${base}.${expose}`, 'must look like "./App"');
|
|
332
|
-
if (!Array.isArray(files)) {
|
|
333
|
-
c.add(`${base}.${expose}`, 'must be an array of URLs');
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
files.forEach((file, i)=>{
|
|
337
|
-
if ('string' != typeof file || !isUrl(file)) c.add(`${base}.${expose}[${String(i)}]`, 'must be an http(s) URL');
|
|
338
|
-
});
|
|
339
|
-
}
|
|
452
|
+
function addIssues(c, issues, at) {
|
|
453
|
+
for (const issue of issues)c.add([
|
|
454
|
+
at,
|
|
455
|
+
issue.path
|
|
456
|
+
].filter(Boolean).join('.'), issue.message);
|
|
340
457
|
}
|
|
341
458
|
function validateRegistryEntry(value) {
|
|
342
459
|
const c = new Checker();
|
|
@@ -350,7 +467,7 @@ function validateRegistryEntry(value) {
|
|
|
350
467
|
});
|
|
351
468
|
const url = c.string(value, 'manifestUrl', '');
|
|
352
469
|
if (void 0 !== url && !isUrl(url)) c.add('manifestUrl', 'must be an http(s) URL');
|
|
353
|
-
|
|
470
|
+
addIssues(c, remoteFileIssues(value), '');
|
|
354
471
|
c.string(value, 'previous', '', {
|
|
355
472
|
required: false,
|
|
356
473
|
pattern: VERSION,
|
|
@@ -395,37 +512,21 @@ function validateRegistryIndex(value) {
|
|
|
395
512
|
return c.issues;
|
|
396
513
|
}
|
|
397
514
|
const names = new Set();
|
|
398
|
-
const claimed =
|
|
515
|
+
const claimed = [];
|
|
399
516
|
remotes.forEach((remote, i)=>{
|
|
400
517
|
const at = `remotes[${String(i)}]`;
|
|
401
518
|
if (!isObj(remote)) return void c.add(at, 'must be an object');
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
c.string(remote, 'owner', at, {
|
|
413
|
-
required: false
|
|
519
|
+
addIssues(c, registryRemoteIssues(remote), at);
|
|
520
|
+
const { name } = remote;
|
|
521
|
+
if ('string' != typeof name || !isRemoteName(name)) return;
|
|
522
|
+
if (names.has(name)) c.add(`${at}.name`, `duplicate remote "${name}"`);
|
|
523
|
+
names.add(name);
|
|
524
|
+
const routes = Array.isArray(remote.routes) ? remote.routes.filter((r)=>'string' == typeof r && ROUTE.test(r)) : [];
|
|
525
|
+
for (const route of routes)for (const other of claimed)if (other.remote !== name && routesOverlap(route, other.route)) c.add(`${at}.routes`, `route "${route}" of "${name}" overlaps route "${other.route}" of "${other.remote}"`, ROUTE_OVERLAP);
|
|
526
|
+
for (const route of routes)claimed.push({
|
|
527
|
+
remote: name,
|
|
528
|
+
route
|
|
414
529
|
});
|
|
415
|
-
const routes = c.stringArray(remote, 'routes', at, {
|
|
416
|
-
required: false,
|
|
417
|
-
pattern: ROUTE,
|
|
418
|
-
hint: 'must start with "/"'
|
|
419
|
-
});
|
|
420
|
-
if (void 0 !== name) {
|
|
421
|
-
if (names.has(name)) c.add(`${at}.name`, `duplicate remote "${name}"`);
|
|
422
|
-
names.add(name);
|
|
423
|
-
for (const route of routes){
|
|
424
|
-
const owner = claimed.get(route);
|
|
425
|
-
if (void 0 !== owner && owner !== name) c.add(`${at}.routes`, `route "${route}" is already claimed by "${owner}"`);
|
|
426
|
-
claimed.set(route, name);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
530
|
});
|
|
430
531
|
return c.issues;
|
|
431
532
|
}
|
|
@@ -528,6 +629,90 @@ function validateFeatureRegistryIndex(value) {
|
|
|
528
629
|
function isFeatureRegistryIndex(value) {
|
|
529
630
|
return 0 === validateFeatureRegistryIndex(value).length;
|
|
530
631
|
}
|
|
632
|
+
function ipv4Octets(host) {
|
|
633
|
+
const parts = host.split('.');
|
|
634
|
+
if (4 !== parts.length) return;
|
|
635
|
+
const octets = parts.map((part)=>/^\d{1,3}$/.test(part) ? Number(part) : 0 / 0);
|
|
636
|
+
return octets.every((n)=>n <= 255) ? octets : void 0;
|
|
637
|
+
}
|
|
638
|
+
function isPrivateIpv4([a = 0, b = 0, c = 0]) {
|
|
639
|
+
return 0 === a || 10 === a || 127 === a || 100 === a && b >= 64 && b <= 127 || 169 === a && 254 === b || 172 === a && b >= 16 && b <= 31 || 192 === a && 168 === b || 192 === a && 0 === b && 0 === c;
|
|
640
|
+
}
|
|
641
|
+
const side = (part)=>'' === part ? [] : part.split(':');
|
|
642
|
+
function ipv6Groups(host) {
|
|
643
|
+
const [head = '', tail, ...rest] = host.split('::');
|
|
644
|
+
if (rest.length > 0) return;
|
|
645
|
+
const headGroups = side(head);
|
|
646
|
+
const groups = [
|
|
647
|
+
...headGroups,
|
|
648
|
+
...void 0 === tail ? [] : side(tail)
|
|
649
|
+
];
|
|
650
|
+
if (groups.some((g)=>!/^[0-9a-f]{1,4}$/.test(g))) return;
|
|
651
|
+
const numbers = groups.map((g)=>Number.parseInt(g, 16));
|
|
652
|
+
if (void 0 === tail) return 8 === numbers.length ? numbers : void 0;
|
|
653
|
+
const missing = 8 - numbers.length;
|
|
654
|
+
if (missing < 1) return;
|
|
655
|
+
return [
|
|
656
|
+
...numbers.slice(0, headGroups.length),
|
|
657
|
+
...Array.from({
|
|
658
|
+
length: missing
|
|
659
|
+
}, ()=>0),
|
|
660
|
+
...numbers.slice(headGroups.length)
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
const embedded = (hi = 0, lo = 0)=>[
|
|
664
|
+
hi >> 8,
|
|
665
|
+
0xff & hi,
|
|
666
|
+
lo >> 8,
|
|
667
|
+
0xff & lo
|
|
668
|
+
];
|
|
669
|
+
function isPrivateIpv6(host) {
|
|
670
|
+
const groups = ipv6Groups(host);
|
|
671
|
+
if (void 0 === groups) return false;
|
|
672
|
+
const [g0 = 0, g1 = 0] = groups;
|
|
673
|
+
const zeros = (from, to)=>groups.slice(from, to).every((g)=>0 === g);
|
|
674
|
+
if (zeros(0, 6)) return isPrivateIpv4(embedded(groups[6], groups[7]));
|
|
675
|
+
if ((0xfe00 & g0) === 0xfc00 || (0xffc0 & g0) === 0xfe80 || (0xffc0 & g0) === 0xfec0) return true;
|
|
676
|
+
const mapped = zeros(0, 5) && 0xffff === groups[5];
|
|
677
|
+
const nat64 = 0x64 === g0 && 0xff9b === g1 && zeros(2, 6);
|
|
678
|
+
if (mapped || nat64) return isPrivateIpv4(embedded(groups[6], groups[7]));
|
|
679
|
+
return 0x2002 === g0 && isPrivateIpv4(embedded(groups[1], groups[2]));
|
|
680
|
+
}
|
|
681
|
+
function isPrivateHost(hostname) {
|
|
682
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
683
|
+
if ('localhost' === host || /\.(?:localhost|internal|local)$/.test(host)) return true;
|
|
684
|
+
if (host.includes(':')) return isPrivateIpv6(host);
|
|
685
|
+
const octets = ipv4Octets(host);
|
|
686
|
+
return void 0 !== octets && isPrivateIpv4(octets);
|
|
687
|
+
}
|
|
688
|
+
const SECRET_PARAM = /token|key|sig|auth|passw|secret|credential|x-amz-|(?:^|[_-])(?:jwt|bearer|session|pat|access|code)(?:$|[_-])/i;
|
|
689
|
+
const URL_PARTS = /^([a-z][a-z0-9+.-]*:\/\/)([^/?#@]*@)?([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
690
|
+
function safeDecode(value) {
|
|
691
|
+
try {
|
|
692
|
+
return decodeURIComponent(value);
|
|
693
|
+
} catch {
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function redactUrl(url) {
|
|
698
|
+
const parts = URL_PARTS.exec(url);
|
|
699
|
+
if (null === parts) return url;
|
|
700
|
+
const [, scheme = '', userinfo, rest = '', query, hash] = parts;
|
|
701
|
+
const search = void 0 === query ? '' : `?${hideSecrets(query.slice(1))}`;
|
|
702
|
+
const fragment = void 0 === hash ? '' : `#${hideSecrets(hash.slice(1))}`;
|
|
703
|
+
return `${scheme}${void 0 === userinfo ? '' : '***@'}${rest}${search}${fragment}`;
|
|
704
|
+
}
|
|
705
|
+
function hideSecrets(params) {
|
|
706
|
+
return params.split(/([&;])/).map((part)=>{
|
|
707
|
+
const eq = part.indexOf('=');
|
|
708
|
+
if (eq <= 0) return part;
|
|
709
|
+
const key = part.slice(0, eq);
|
|
710
|
+
return SECRET_PARAM.test(safeDecode(key)) ? `${key}=***` : part;
|
|
711
|
+
}).join('');
|
|
712
|
+
}
|
|
713
|
+
function redactUrlsIn(text) {
|
|
714
|
+
return text.replaceAll(/https?:\/\/[^\s"'<>]+/gi, (found)=>redactUrl(found));
|
|
715
|
+
}
|
|
531
716
|
const project_SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\u0000]+`;
|
|
532
717
|
const CHECKOUT_PATH = new RegExp(`^(?:(?![A-Za-z]:)(?:\\.\\.(?:/\\.\\.)*(?:/${project_SEGMENT})*|${project_SEGMENT}(?:/${project_SEGMENT})*)|(?:/|[A-Za-z]:/)${project_SEGMENT}(?:/${project_SEGMENT})*)$`);
|
|
533
718
|
const FILE_HASH = /^sha256-[0-9a-f]{64}$/;
|
|
@@ -545,7 +730,14 @@ function isCheckoutPath(value) {
|
|
|
545
730
|
const rest = -1 === climbing ? [] : parts.slice(climbing);
|
|
546
731
|
return rest.every((part)=>'' !== part && '.' !== part && '..' !== part);
|
|
547
732
|
}
|
|
733
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
734
|
+
const REGISTRY_HOST = /^(?:file:\S.*|s3:\/\/[a-z0-9][a-z0-9.-]{1,61}[a-z0-9](?:\/\S*)?)$/;
|
|
548
735
|
const SCOPE = /^@[a-z0-9][a-z0-9-._]*$/;
|
|
736
|
+
const PROJECT_REPOS = [
|
|
737
|
+
'platform',
|
|
738
|
+
'shell',
|
|
739
|
+
'remote'
|
|
740
|
+
];
|
|
549
741
|
const FEATURE = /^(@[a-z0-9-]+\/)?[a-z][a-z0-9-]*$/;
|
|
550
742
|
function validateMfeflowProject(value) {
|
|
551
743
|
const c = new Checker();
|
|
@@ -554,6 +746,18 @@ function validateMfeflowProject(value) {
|
|
|
554
746
|
return c.issues;
|
|
555
747
|
}
|
|
556
748
|
if (1 !== value.version) c.add('version', 'must be 1');
|
|
749
|
+
if ('cli' in value) c.string(value, 'cli', '', {
|
|
750
|
+
required: false,
|
|
751
|
+
pattern: SEMVER,
|
|
752
|
+
hint: 'must be a version like "0.5.0"'
|
|
753
|
+
});
|
|
754
|
+
let repo;
|
|
755
|
+
if ('repo' in value) {
|
|
756
|
+
repo = c.string(value, 'repo', '', {
|
|
757
|
+
required: false
|
|
758
|
+
});
|
|
759
|
+
if (void 0 !== repo && !PROJECT_REPOS.some((name)=>name === repo)) c.add('repo', 'must be platform, shell or remote');
|
|
760
|
+
}
|
|
557
761
|
const topology = c.string(value, 'topology', '');
|
|
558
762
|
if (void 0 !== topology && ![
|
|
559
763
|
'monorepo',
|
|
@@ -590,7 +794,7 @@ function validateMfeflowProject(value) {
|
|
|
590
794
|
});
|
|
591
795
|
if (void 0 !== port && !Number.isInteger(port)) c.add(`${at}.port`, 'must be a whole number');
|
|
592
796
|
}
|
|
593
|
-
if (0 === shells) c.add('apps', 'needs at least one shell');
|
|
797
|
+
if (0 === shells && 'remote' !== repo && 'platform' !== repo) c.add('apps', 'needs at least one shell');
|
|
594
798
|
}
|
|
595
799
|
const features = c.object(value, 'features', '');
|
|
596
800
|
if (features) for (const [name, feature] of Object.entries(features)){
|
|
@@ -599,6 +803,10 @@ function validateMfeflowProject(value) {
|
|
|
599
803
|
if (isObj(feature)) {
|
|
600
804
|
c.string(feature, 'version', at);
|
|
601
805
|
c.object(feature, 'options', at, false);
|
|
806
|
+
if ('integrity' in feature) {
|
|
807
|
+
const integrity = c.string(feature, 'integrity', at);
|
|
808
|
+
if (void 0 !== integrity && !FILE_HASH.test(integrity)) c.add(`${at}.integrity`, 'must be a hash like "sha256-<64 hex characters>"');
|
|
809
|
+
}
|
|
602
810
|
} else c.add(at, 'must be an object');
|
|
603
811
|
}
|
|
604
812
|
const files = c.object(value, 'files', '', false);
|
|
@@ -607,6 +815,12 @@ function validateMfeflowProject(value) {
|
|
|
607
815
|
if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
|
|
608
816
|
if ('string' != typeof hash || !FILE_HASH.test(hash)) c.add(at, 'must be a hash like "sha256-<64 hex characters>"');
|
|
609
817
|
}
|
|
818
|
+
const kept = c.object(value, 'kept', '', false);
|
|
819
|
+
if (kept) for (const [file, hash] of Object.entries(kept)){
|
|
820
|
+
const at = `kept.${file}`;
|
|
821
|
+
if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
|
|
822
|
+
if ('string' != typeof hash || !FILE_HASH.test(hash)) c.add(at, 'must be a hash like "sha256-<64 hex characters>"');
|
|
823
|
+
}
|
|
610
824
|
const registries = c.object(value, 'registries', '', false);
|
|
611
825
|
if (registries) for (const [ns, url] of Object.entries(registries)){
|
|
612
826
|
const at = `registries.${ns}`;
|
|
@@ -632,51 +846,27 @@ function validateMfeflowProject(value) {
|
|
|
632
846
|
required: false,
|
|
633
847
|
min: 1
|
|
634
848
|
});
|
|
849
|
+
const registry = c.object(value, 'registry', '', false);
|
|
850
|
+
if (registry) {
|
|
851
|
+
c.string(registry, 'host', 'registry', {
|
|
852
|
+
required: false,
|
|
853
|
+
pattern: REGISTRY_HOST,
|
|
854
|
+
hint: 'must be file:<dir> or s3://<bucket>[/prefix], with no credentials'
|
|
855
|
+
});
|
|
856
|
+
const baseUrl = c.string(registry, 'baseUrl', 'registry', {
|
|
857
|
+
required: false
|
|
858
|
+
});
|
|
859
|
+
if (void 0 !== baseUrl && !(isUrl(baseUrl) && validBaseUrl(baseUrl))) c.add('registry.baseUrl', 'must be an https URL (http only on localhost) without credentials, a query, a fragment or a trailing slash');
|
|
860
|
+
}
|
|
635
861
|
return c.issues;
|
|
636
862
|
}
|
|
863
|
+
function validBaseUrl(value) {
|
|
864
|
+
const url = new URL(value);
|
|
865
|
+
return isRegistryProtocol(url) && '' === url.username && '' === url.password && !/[?#]/.test(value) && !value.endsWith('/');
|
|
866
|
+
}
|
|
637
867
|
function isMfeflowProject(value) {
|
|
638
868
|
return 0 === validateMfeflowProject(value).length;
|
|
639
869
|
}
|
|
640
|
-
const COST_MODEL = {
|
|
641
|
-
lcpDeltaMs: 300,
|
|
642
|
-
shellKb: 32,
|
|
643
|
-
remoteKb: 10
|
|
644
|
-
};
|
|
645
|
-
const KB = 1024;
|
|
646
|
-
const round1 = (value)=>Math.round(10 * value) / 10;
|
|
647
|
-
const line = (actual, budget)=>({
|
|
648
|
-
budget,
|
|
649
|
-
actual,
|
|
650
|
-
pass: actual <= budget
|
|
651
|
-
});
|
|
652
|
-
function judgeCost(sample, model = COST_MODEL) {
|
|
653
|
-
const lcp = line(Math.round(sample.lcpDeltaMs), model.lcpDeltaMs);
|
|
654
|
-
const shell = line(round1((sample.shellBytes - sample.monolithBytes) / KB), model.shellKb);
|
|
655
|
-
const remotes = Object.entries(sample.remoteBytes).toSorted(([a], [b])=>a < b ? -1 : a > b ? 1 : 0).map(([name, bytes])=>Object.assign(line(round1(bytes / KB), model.remoteKb), {
|
|
656
|
-
name
|
|
657
|
-
}));
|
|
658
|
-
return {
|
|
659
|
-
pass: lcp.pass && shell.pass && remotes.every((r)=>r.pass),
|
|
660
|
-
lcp,
|
|
661
|
-
shell,
|
|
662
|
-
remotes
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
const REMOTE_PATH = /^\/mfe\/([^/]+)\//;
|
|
666
|
-
function splitScripts(requests) {
|
|
667
|
-
let shellBytes = 0;
|
|
668
|
-
const remoteBytes = {};
|
|
669
|
-
for (const request of requests){
|
|
670
|
-
if ('Script' !== request.type) continue;
|
|
671
|
-
const remote = REMOTE_PATH.exec(request.path)?.[1];
|
|
672
|
-
if (void 0 === remote) shellBytes += request.bytes;
|
|
673
|
-
else remoteBytes[remote] = (remoteBytes[remote] ?? 0) + request.bytes;
|
|
674
|
-
}
|
|
675
|
-
return {
|
|
676
|
-
shellBytes,
|
|
677
|
-
remoteBytes
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
870
|
const TRANSFORM_OPS = [
|
|
681
871
|
'add-import',
|
|
682
872
|
'wrap-provider',
|
|
@@ -964,13 +1154,16 @@ function isFeatureManifest(value) {
|
|
|
964
1154
|
}
|
|
965
1155
|
const DOCS_URL = 'https://mfeflow.github.io/mfeflow';
|
|
966
1156
|
const SCHEMA_URLS = {
|
|
967
|
-
mfe: `${DOCS_URL}/schema/
|
|
1157
|
+
mfe: `${DOCS_URL}/schema/mfeflow-app.json`,
|
|
968
1158
|
project: `${DOCS_URL}/schema/mfeflow.json`,
|
|
969
1159
|
feature: `${DOCS_URL}/schema/feature.json`,
|
|
970
1160
|
registryEntry: `${DOCS_URL}/schema/registry-entry.json`,
|
|
971
1161
|
registryIndex: `${DOCS_URL}/schema/registry-index.json`,
|
|
972
1162
|
featureRegistryIndex: `${DOCS_URL}/schema/feature-registry-index.json`
|
|
973
1163
|
};
|
|
1164
|
+
const SCHEMA_ALIASES = {
|
|
1165
|
+
[`${DOCS_URL}/schema/mfe.json`]: 'mfe'
|
|
1166
|
+
};
|
|
974
1167
|
function errorDocsUrl(code) {
|
|
975
1168
|
return `${DOCS_URL}/errors/${code}`;
|
|
976
1169
|
}
|
|
@@ -1210,7 +1403,7 @@ const json_schema_item = (properties, required)=>({
|
|
|
1210
1403
|
required
|
|
1211
1404
|
});
|
|
1212
1405
|
const JSON_SCHEMAS = {
|
|
1213
|
-
mfe: root('mfe', '
|
|
1406
|
+
mfe: root('mfe', 'mfeflow.app.json', {
|
|
1214
1407
|
properties: {
|
|
1215
1408
|
name: str(MFE_NAME),
|
|
1216
1409
|
owner: str(),
|
|
@@ -1266,6 +1459,12 @@ const JSON_SCHEMAS = {
|
|
|
1266
1459
|
version: {
|
|
1267
1460
|
const: 1
|
|
1268
1461
|
},
|
|
1462
|
+
cli: str(SEMVER),
|
|
1463
|
+
repo: {
|
|
1464
|
+
enum: [
|
|
1465
|
+
...PROJECT_REPOS
|
|
1466
|
+
]
|
|
1467
|
+
},
|
|
1269
1468
|
topology: {
|
|
1270
1469
|
enum: [
|
|
1271
1470
|
'monorepo',
|
|
@@ -1309,7 +1508,8 @@ const JSON_SCHEMAS = {
|
|
|
1309
1508
|
version: str(),
|
|
1310
1509
|
options: {
|
|
1311
1510
|
type: 'object'
|
|
1312
|
-
}
|
|
1511
|
+
},
|
|
1512
|
+
integrity: str(FILE_HASH)
|
|
1313
1513
|
}, [
|
|
1314
1514
|
'version'
|
|
1315
1515
|
])
|
|
@@ -1319,6 +1519,11 @@ const JSON_SCHEMAS = {
|
|
|
1319
1519
|
propertyNames: relativePath,
|
|
1320
1520
|
additionalProperties: str(FILE_HASH)
|
|
1321
1521
|
},
|
|
1522
|
+
kept: {
|
|
1523
|
+
type: 'object',
|
|
1524
|
+
propertyNames: relativePath,
|
|
1525
|
+
additionalProperties: str(FILE_HASH)
|
|
1526
|
+
},
|
|
1322
1527
|
registries: {
|
|
1323
1528
|
type: 'object',
|
|
1324
1529
|
propertyNames: {
|
|
@@ -1348,6 +1553,16 @@ const JSON_SCHEMAS = {
|
|
|
1348
1553
|
minimum: 1
|
|
1349
1554
|
}
|
|
1350
1555
|
]))
|
|
1556
|
+
},
|
|
1557
|
+
registry: {
|
|
1558
|
+
type: 'object',
|
|
1559
|
+
properties: {
|
|
1560
|
+
host: str(REGISTRY_HOST),
|
|
1561
|
+
baseUrl: {
|
|
1562
|
+
type: 'string',
|
|
1563
|
+
pattern: String.raw`^(?:https://[^@\s?#]*[^/@\s?#]|http://(?:127\.0\.0\.1|localhost|\[::1\])(?::\d+)?(?:/[^@\s?#]*[^/@\s?#])?)$`
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1351
1566
|
}
|
|
1352
1567
|
},
|
|
1353
1568
|
required: [
|
|
@@ -1511,7 +1726,10 @@ const JSON_SCHEMAS = {
|
|
|
1511
1726
|
...remoteFiles,
|
|
1512
1727
|
version: str(),
|
|
1513
1728
|
routes: json_schema_strings(ROUTE),
|
|
1514
|
-
owner: str()
|
|
1729
|
+
owner: str(),
|
|
1730
|
+
previous: str(),
|
|
1731
|
+
history: json_schema_strings(),
|
|
1732
|
+
rolledBackFrom: str()
|
|
1515
1733
|
}, [
|
|
1516
1734
|
'name',
|
|
1517
1735
|
'entry'
|
|
@@ -1554,4 +1772,5 @@ const JSON_SCHEMAS = {
|
|
|
1554
1772
|
]
|
|
1555
1773
|
})
|
|
1556
1774
|
};
|
|
1557
|
-
export {
|
|
1775
|
+
export { EXPOSE, ROUTE, isRegistryIndexRemote, registryRemoteIssues } from "./entry.js";
|
|
1776
|
+
export { ANALYTICS_EVENT, COST_MODEL, DOCS_URL, EVENT_NAME, FEATURE_CONDITION, FEATURE_NAME, FILE_HASH, JSON_SCHEMAS, MAX_DESCRIPTION, MAX_INDEX_ITEMS, MFE_NAME, PROJECT_REPOS, REGISTRY_HOST, REGISTRY_ITEM_NAME, REGISTRY_NAMESPACE, ROUTE_OVERLAP, SCHEMA_ALIASES, SCHEMA_URLS, TRANSFORM_OPS, assetsFromManifest, errorDocsUrl, injectIntoHead, isFeatureManifest, isFeatureRegistryIndex, isLoopbackHttp, isMfeManifest, isMfeflowProject, isPortableRelativePath, isPrivateHost, isRegistryEntry, isRegistryIndex, isRegistryProtocol, isSafeRelativePath, judgeCost, judgeRemoteInitial, primaryExpose, redactUrl, redactUrlsIn, registryIndexUrl, registryInjection, registryTemplateProblem, remoteDistFiles, round1, routesOverlap, splitScripts, validateFeatureManifest, validateFeatureRegistryIndex, validateMfeManifest, validateMfeflowProject, validateRegistryEntry, validateRegistryIndex };
|
package/dist/mfe-manifest.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { EXPOSE, ROUTE } from './entry.js';
|
|
1
2
|
import { type Issue } from './validate.js';
|
|
2
|
-
|
|
3
|
+
export { EXPOSE, ROUTE };
|
|
4
|
+
/** `mfeflow.app.json` (formerly `mfe.json`): who owns a micro-frontend and what it promises. */
|
|
3
5
|
export interface MfeManifest {
|
|
4
6
|
$schema?: string;
|
|
5
7
|
/** Module Federation name: lowercase letters, digits, `_`. */
|
|
@@ -32,8 +34,6 @@ export interface MfeManifest {
|
|
|
32
34
|
* cannot become the same component or global name.
|
|
33
35
|
*/
|
|
34
36
|
export declare const MFE_NAME: RegExp;
|
|
35
|
-
export declare const ROUTE: RegExp;
|
|
36
|
-
export declare const EXPOSE: RegExp;
|
|
37
37
|
/** `<domain>.<event>.v<major>`, e.g. `cart.item-added.v1`. The same pattern `@mfeflow/event-bus` checks names against. */
|
|
38
38
|
export declare const EVENT_NAME: RegExp;
|
|
39
39
|
/** `<domain>_<object>_<action>`, e.g. `checkout_order_completed`. */
|
package/dist/project.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { type Issue } from './validate.js';
|
|
|
3
3
|
export interface MfeflowProject {
|
|
4
4
|
$schema?: string;
|
|
5
5
|
version: 1;
|
|
6
|
+
/** Version of the CLI that last wrote this file. */
|
|
7
|
+
cli?: string;
|
|
8
|
+
/** Which repo this is in a multi-repo setup. Absent means a monorepo. A `platform` or `remote` repo may have no shell. */
|
|
9
|
+
repo?: 'platform' | 'shell' | 'remote';
|
|
6
10
|
topology: 'monorepo' | 'polyrepo' | 'hybrid';
|
|
7
11
|
packageManager: 'pnpm' | 'npm';
|
|
8
12
|
/** npm scope for the project's own packages, e.g. `@shopco`. */
|
|
@@ -15,15 +19,30 @@ export interface MfeflowProject {
|
|
|
15
19
|
features: Record<string, {
|
|
16
20
|
version: string;
|
|
17
21
|
options?: Record<string, unknown>;
|
|
22
|
+
/** `sha256-<hex>` of the `feature.json` a registry served when the feature was added. Third-party features only. */
|
|
23
|
+
integrity?: string;
|
|
18
24
|
}>;
|
|
19
25
|
/** Hashes of copied files, so `upgrade` can tell when a file was edited. */
|
|
20
26
|
files?: Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Files `upgrade --keep` left alone: the hash of the new version each one was kept against.
|
|
29
|
+
* While the new version is that one, the file stays quiet; when it changes, the file is raised again.
|
|
30
|
+
*/
|
|
31
|
+
kept?: Record<string, string>;
|
|
21
32
|
/** Third-party feature registries: `{ "@someone": "https://…/{name}.json" }`, https only (plain http for loopback hosts). `@mfeflow` is reserved. */
|
|
22
33
|
registries?: Record<string, string>;
|
|
23
34
|
/** Built mfeflow checkout, relative to the project root, that `@mfeflow/*` dependencies link to. Set by `--local` only. */
|
|
24
35
|
local?: string;
|
|
25
36
|
/** Overrides of the cost-model budget `mfeflow budget` and the perf job check. */
|
|
26
37
|
budget?: ProjectBudget;
|
|
38
|
+
/** Where `mfeflow deploy`, `promote`, `rollback` and `status` publish by default. */
|
|
39
|
+
registry?: ProjectRegistry;
|
|
40
|
+
}
|
|
41
|
+
export interface ProjectRegistry {
|
|
42
|
+
/** `file:<dir>` for a folder, or `s3://<bucket>[/prefix]`. Credentials never go here; they come from the environment. */
|
|
43
|
+
host?: string;
|
|
44
|
+
/** Where the host's files are served from (`https://cdn.example.com`), without a trailing slash. */
|
|
45
|
+
baseUrl?: string;
|
|
27
46
|
}
|
|
28
47
|
export interface ProjectBudget {
|
|
29
48
|
/** JS the shell may add over a one-app build, in KB. Default 32. */
|
|
@@ -41,6 +60,10 @@ export declare const FILE_HASH: RegExp;
|
|
|
41
60
|
export declare const REGISTRY_NAMESPACE: RegExp;
|
|
42
61
|
/** A relative path with forward slashes whose only `..` segments come first, so it may leave the project. */
|
|
43
62
|
export declare function isCheckoutPath(value: string): boolean;
|
|
63
|
+
export declare const SEMVER: RegExp;
|
|
64
|
+
/** `file:<dir>` or `s3://<bucket>[/prefix]`. No credentials: `@` and `:` cannot appear in a bucket name. */
|
|
65
|
+
export declare const REGISTRY_HOST: RegExp;
|
|
44
66
|
export declare const SCOPE: RegExp;
|
|
67
|
+
export declare const PROJECT_REPOS: readonly ['platform', 'shell', 'remote'];
|
|
45
68
|
export declare function validateMfeflowProject(value: unknown): Issue[];
|
|
46
69
|
export declare function isMfeflowProject(value: unknown): value is MfeflowProject;
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isRegistryIndexRemote, registryRemoteIssues, type RegistryIndex, type RegistryIndexRemote } from './entry.js';
|
|
1
2
|
import { type Issue } from './validate.js';
|
|
3
|
+
export { isRegistryIndexRemote, registryRemoteIssues, type RegistryIndex, type RegistryIndexRemote, };
|
|
2
4
|
/** `registry/envs/<env>/<name>.json`: what one remote serves in one environment. */
|
|
3
5
|
export interface RegistryEntry {
|
|
4
6
|
version: string;
|
|
@@ -19,29 +21,8 @@ export interface RegistryEntry {
|
|
|
19
21
|
percent: number;
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
24
|
-
shell?: string;
|
|
25
|
-
env?: string;
|
|
26
|
-
generatedAt?: string;
|
|
27
|
-
remotes: RegistryIndexRemote[];
|
|
28
|
-
}
|
|
29
|
-
export interface RegistryIndexRemote {
|
|
30
|
-
name: string;
|
|
31
|
-
/** `mf-manifest.json` URL. */
|
|
32
|
-
entry: string;
|
|
33
|
-
/** Entry script URL; shells register it in place of `entry` when present. */
|
|
34
|
-
remoteEntry?: string;
|
|
35
|
-
/** How `remoteEntry` loads (`module` for ESM). */
|
|
36
|
-
type?: string;
|
|
37
|
-
/** The global `remoteEntry` defines when it differs from `name`. */
|
|
38
|
-
entryGlobalName?: string;
|
|
39
|
-
/** Per expose, the files to preload. Not inlined into the shell's HTML. */
|
|
40
|
-
exposes?: Record<string, string[]>;
|
|
41
|
-
version?: string;
|
|
42
|
-
routes?: string[];
|
|
43
|
-
owner?: string;
|
|
44
|
-
}
|
|
24
|
+
/** The `code` of the issues `validateRegistryIndex` reports for overlapping routes. */
|
|
25
|
+
export declare const ROUTE_OVERLAP = "route-overlap";
|
|
45
26
|
export declare const VERSION: RegExp;
|
|
46
27
|
export declare function validateRegistryEntry(value: unknown): Issue[];
|
|
47
28
|
export declare function validateRegistryIndex(value: unknown): Issue[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CostModel } from './cost-model.js';
|
|
2
|
+
/** The expose a remote's route loads first: `./App`, else the first one. */
|
|
3
|
+
export declare function primaryExpose(names: readonly string[]): string | undefined;
|
|
4
|
+
export interface RemoteInitialVerdict {
|
|
5
|
+
/** Gzip KB that were judged, one decimal: the entry plus the primary expose, or the entry alone without an app file budget. */
|
|
6
|
+
kb: number;
|
|
7
|
+
/** Gzip KB of the entry plus the primary expose, whichever was judged. */
|
|
8
|
+
totalKb: number;
|
|
9
|
+
limitKb: number;
|
|
10
|
+
pass: boolean;
|
|
11
|
+
/** Where the limit came from: the remote's `mfeflow.app.json` budget or the cost model. */
|
|
12
|
+
source: 'mfeflow.app.json' | 'cost-model';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Judges the JS a remote loads first. With a `mfeflow.app.json` `budgets.initialJsKb` the entry plus the
|
|
16
|
+
* primary expose must fit it; without one the entry alone must fit the cost model's `remoteKb`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function judgeRemoteInitial(bytes: {
|
|
19
|
+
entryBytes: number;
|
|
20
|
+
exposeBytes: number;
|
|
21
|
+
}, budgetKb: number | undefined, model?: CostModel): RemoteInitialVerdict;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RemoteDistFiles {
|
|
2
|
+
/** The entry script, as a path inside the build folder. */
|
|
3
|
+
entry: string;
|
|
4
|
+
/** expose (`./App`) → the JS files it loads before it renders, as paths inside the build folder. */
|
|
5
|
+
exposes: Record<string, string[]>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The files a remote's build folder holds for its entry and each expose's synchronous JS, read from
|
|
9
|
+
* its `mf-manifest.json`. Paths resolve against the folder whatever `publicPath` says about where the
|
|
10
|
+
* files are served. Undefined when the manifest has no usable entry.
|
|
11
|
+
*/
|
|
12
|
+
export declare function remoteDistFiles(manifest: unknown): RemoteDistFiles | undefined;
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when some URL path matches both routes. Routes compare case-insensitively and ignore a
|
|
3
|
+
* trailing slash. A `*` on the last segment (`/checkout/*`, `/checkout*`, `/:rest*`) matches any
|
|
4
|
+
* tail, including none; anywhere else it stays inside one segment. `:param` matches one segment.
|
|
5
|
+
*/
|
|
6
|
+
export declare function routesOverlap(a: string, b: string): boolean;
|
package/dist/validate.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ export interface Issue {
|
|
|
2
2
|
/** Where, e.g. `routes[0]` or `rollout.percent`. Empty for the root. */
|
|
3
3
|
path: string;
|
|
4
4
|
message: string;
|
|
5
|
+
/** Set on issues a caller may want to tell apart, e.g. `route-overlap`. */
|
|
6
|
+
code?: string;
|
|
5
7
|
}
|
|
6
8
|
export type Obj = Record<string, unknown>;
|
|
7
9
|
export declare function isObj(value: unknown): value is Obj;
|
|
8
10
|
export declare class Checker {
|
|
9
11
|
readonly issues: Issue[];
|
|
10
|
-
add(path: string, message: string): void;
|
|
12
|
+
add(path: string, message: string, code?: string): void;
|
|
11
13
|
/** Reports a missing or wrong-typed string and returns it when valid. */
|
|
12
14
|
string(obj: Obj, key: string, path: string, opts?: {
|
|
13
15
|
required?: boolean;
|
|
@@ -30,3 +32,8 @@ export declare function isUrl(value: string): boolean;
|
|
|
30
32
|
/** The regex form of `isSafeRelativePath`, for JSON Schema. */
|
|
31
33
|
export declare const SAFE_RELATIVE_PATH: RegExp;
|
|
32
34
|
export declare function isSafeRelativePath(value: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* `isSafeRelativePath` plus what a file on any OS can be named: no `:` (drive letters, NTFS
|
|
37
|
+
* streams), no Windows device names (`CON`, `nul.txt`, `COM1`) and no segment ending in a dot or space.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isPortableRelativePath(value: string): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/contracts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Types, validators and JSON Schemas for mfeflow:
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Types, validators and JSON Schemas for mfeflow: mfeflow.app.json ownership manifests, mfeflow.json project state, and the remote registry (entries and index).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|
|
7
7
|
"mfe",
|