@mfeflow/contracts 0.5.0 → 0.7.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 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 `mfe.json`, `mfeflow.json` and the remote registry. Pre-1.0: minor versions may change the API.
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
- readonly mfe: "https://mfeflow.github.io/mfeflow/schema/mfe.json";
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/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export { judgeRemoteInitial, primaryExpose, type RemoteInitialVerdict } from './
7
7
  export { injectIntoHead, registryInjection, type InjectionOptions, type RouteNeed, } from './html-injection.js';
8
8
  export { isRegistryEntry, isRegistryIndex, isRegistryIndexRemote, registryRemoteIssues, ROUTE_OVERLAP, validateRegistryEntry, validateRegistryIndex, type RegistryEntry, type RegistryIndex, type RegistryIndexRemote, } from './registry.js';
9
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';
10
+ export { ENV_KEY_NAME, FILE_HASH, PROJECT_REPOS, REGISTRY_HOST, REGISTRY_NAMESPACE, isMfeflowProject, validateMfeflowProject, type MfeflowProject, type ProjectBudget, type ProjectRegistry, } from './project.js';
11
11
  export { COST_MODEL, judgeCost, round1, splitScripts, type CostLine, type CostModel, type CostSample, type CostVerdict, } from './cost-model.js';
12
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';
13
- 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';
14
14
  export { JSON_SCHEMAS } from './json-schema.js';
package/dist/index.js CHANGED
@@ -349,7 +349,7 @@ function judgeRemoteInitial(bytes, budgetKb, model = COST_MODEL) {
349
349
  totalKb,
350
350
  limitKb: budgetKb,
351
351
  pass: totalKb <= budgetKb,
352
- source: 'mfe.json'
352
+ source: 'mfeflow.app.json'
353
353
  };
354
354
  const kb = round1(bytes.entryBytes / remote_budget_KB);
355
355
  return {
@@ -715,6 +715,7 @@ function redactUrlsIn(text) {
715
715
  }
716
716
  const project_SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\u0000]+`;
717
717
  const CHECKOUT_PATH = new RegExp(`^(?:(?![A-Za-z]:)(?:\\.\\.(?:/\\.\\.)*(?:/${project_SEGMENT})*|${project_SEGMENT}(?:/${project_SEGMENT})*)|(?:/|[A-Za-z]:/)${project_SEGMENT}(?:/${project_SEGMENT})*)$`);
718
+ const ENV_KEY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
718
719
  const FILE_HASH = /^sha256-[0-9a-f]{64}$/;
719
720
  const REGISTRY_NAMESPACE = /^@[a-z0-9][a-z0-9-]*$/;
720
721
  function isCheckoutPath(value) {
@@ -794,7 +795,7 @@ function validateMfeflowProject(value) {
794
795
  });
795
796
  if (void 0 !== port && !Number.isInteger(port)) c.add(`${at}.port`, 'must be a whole number');
796
797
  }
797
- if (0 === shells && 'remote' !== repo) c.add('apps', 'needs at least one shell');
798
+ if (0 === shells && 'remote' !== repo && 'platform' !== repo) c.add('apps', 'needs at least one shell');
798
799
  }
799
800
  const features = c.object(value, 'features', '');
800
801
  if (features) for (const [name, feature] of Object.entries(features)){
@@ -815,6 +816,18 @@ function validateMfeflowProject(value) {
815
816
  if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
816
817
  if ('string' != typeof hash || !FILE_HASH.test(hash)) c.add(at, 'must be a hash like "sha256-<64 hex characters>"');
817
818
  }
819
+ const kept = c.object(value, 'kept', '', false);
820
+ if (kept) for (const [file, hash] of Object.entries(kept)){
821
+ const at = `kept.${file}`;
822
+ if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
823
+ if ('string' != typeof hash || !FILE_HASH.test(hash)) c.add(at, 'must be a hash like "sha256-<64 hex characters>"');
824
+ }
825
+ const envKeys = c.object(value, 'envKeys', '', false);
826
+ if (envKeys) for (const [file, keys] of Object.entries(envKeys)){
827
+ const at = `envKeys.${file}`;
828
+ if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
829
+ if (!Array.isArray(keys) || keys.some((key)=>'string' != typeof key || !ENV_KEY_NAME.test(key))) c.add(at, 'must be a list of key names like "API_ORIGIN"');
830
+ }
818
831
  const registries = c.object(value, 'registries', '', false);
819
832
  if (registries) for (const [ns, url] of Object.entries(registries)){
820
833
  const at = `registries.${ns}`;
@@ -1148,13 +1161,16 @@ function isFeatureManifest(value) {
1148
1161
  }
1149
1162
  const DOCS_URL = 'https://mfeflow.github.io/mfeflow';
1150
1163
  const SCHEMA_URLS = {
1151
- mfe: `${DOCS_URL}/schema/mfe.json`,
1164
+ mfe: `${DOCS_URL}/schema/mfeflow-app.json`,
1152
1165
  project: `${DOCS_URL}/schema/mfeflow.json`,
1153
1166
  feature: `${DOCS_URL}/schema/feature.json`,
1154
1167
  registryEntry: `${DOCS_URL}/schema/registry-entry.json`,
1155
1168
  registryIndex: `${DOCS_URL}/schema/registry-index.json`,
1156
1169
  featureRegistryIndex: `${DOCS_URL}/schema/feature-registry-index.json`
1157
1170
  };
1171
+ const SCHEMA_ALIASES = {
1172
+ [`${DOCS_URL}/schema/mfe.json`]: 'mfe'
1173
+ };
1158
1174
  function errorDocsUrl(code) {
1159
1175
  return `${DOCS_URL}/errors/${code}`;
1160
1176
  }
@@ -1394,7 +1410,7 @@ const json_schema_item = (properties, required)=>({
1394
1410
  required
1395
1411
  });
1396
1412
  const JSON_SCHEMAS = {
1397
- mfe: root('mfe', 'mfe.json', {
1413
+ mfe: root('mfe', 'mfeflow.app.json', {
1398
1414
  properties: {
1399
1415
  name: str(MFE_NAME),
1400
1416
  owner: str(),
@@ -1510,6 +1526,16 @@ const JSON_SCHEMAS = {
1510
1526
  propertyNames: relativePath,
1511
1527
  additionalProperties: str(FILE_HASH)
1512
1528
  },
1529
+ kept: {
1530
+ type: 'object',
1531
+ propertyNames: relativePath,
1532
+ additionalProperties: str(FILE_HASH)
1533
+ },
1534
+ envKeys: {
1535
+ type: 'object',
1536
+ propertyNames: relativePath,
1537
+ additionalProperties: json_schema_strings(ENV_KEY_NAME)
1538
+ },
1513
1539
  registries: {
1514
1540
  type: 'object',
1515
1541
  propertyNames: {
@@ -1759,4 +1785,4 @@ const JSON_SCHEMAS = {
1759
1785
  })
1760
1786
  };
1761
1787
  export { EXPOSE, ROUTE, isRegistryIndexRemote, registryRemoteIssues } from "./entry.js";
1762
- 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_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 };
1788
+ export { ANALYTICS_EVENT, COST_MODEL, DOCS_URL, ENV_KEY_NAME, 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 };
@@ -1,7 +1,7 @@
1
1
  import { EXPOSE, ROUTE } from './entry.js';
2
2
  import { type Issue } from './validate.js';
3
3
  export { EXPOSE, ROUTE };
4
- /** `mfe.json`: who owns a micro-frontend and what it promises. */
4
+ /** `mfeflow.app.json` (formerly `mfe.json`): who owns a micro-frontend and what it promises. */
5
5
  export interface MfeManifest {
6
6
  $schema?: string;
7
7
  /** Module Federation name: lowercase letters, digits, `_`. */
package/dist/project.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface MfeflowProject {
5
5
  version: 1;
6
6
  /** Version of the CLI that last wrote this file. */
7
7
  cli?: string;
8
- /** Which repo this is in a multi-repo setup. Absent means a platform monorepo. */
8
+ /** Which repo this is in a multi-repo setup. Absent means a monorepo. A `platform` or `remote` repo may have no shell. */
9
9
  repo?: 'platform' | 'shell' | 'remote';
10
10
  topology: 'monorepo' | 'polyrepo' | 'hybrid';
11
11
  packageManager: 'pnpm' | 'npm';
@@ -24,6 +24,16 @@ export interface MfeflowProject {
24
24
  }>;
25
25
  /** Hashes of copied files, so `upgrade` can tell when a file was edited. */
26
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>;
32
+ /**
33
+ * Keys `mfeflow add env-key` wrote, by the file it wrote them in. `upgrade` asks before a new
34
+ * template changes the value of one of them. Absent in projects made before this was recorded.
35
+ */
36
+ envKeys?: Record<string, string[]>;
27
37
  /** Third-party feature registries: `{ "@someone": "https://…/{name}.json" }`, https only (plain http for loopback hosts). `@mfeflow` is reserved. */
28
38
  registries?: Record<string, string>;
29
39
  /** Built mfeflow checkout, relative to the project root, that `@mfeflow/*` dependencies link to. Set by `--local` only. */
@@ -51,6 +61,8 @@ export interface ProjectBudget {
51
61
  }
52
62
  /** The regex form of `isCheckoutPath`, for JSON Schema. */
53
63
  export declare const CHECKOUT_PATH: RegExp;
64
+ /** An env key name, as `mfeflow add env-key` accepts it. */
65
+ export declare const ENV_KEY_NAME: RegExp;
54
66
  export declare const FILE_HASH: RegExp;
55
67
  export declare const REGISTRY_NAMESPACE: RegExp;
56
68
  /** A relative path with forward slashes whose only `..` segments come first, so it may leave the project. */
@@ -2,17 +2,17 @@ import { type CostModel } from './cost-model.js';
2
2
  /** The expose a remote's route loads first: `./App`, else the first one. */
3
3
  export declare function primaryExpose(names: readonly string[]): string | undefined;
4
4
  export interface RemoteInitialVerdict {
5
- /** Gzip KB that were judged, one decimal: the entry plus the primary expose, or the entry alone without an `mfe.json` budget. */
5
+ /** Gzip KB that were judged, one decimal: the entry plus the primary expose, or the entry alone without an app file budget. */
6
6
  kb: number;
7
7
  /** Gzip KB of the entry plus the primary expose, whichever was judged. */
8
8
  totalKb: number;
9
9
  limitKb: number;
10
10
  pass: boolean;
11
- /** Where the limit came from: the remote's `mfe.json` budget or the cost model. */
12
- source: 'mfe.json' | 'cost-model';
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
13
  }
14
14
  /**
15
- * Judges the JS a remote loads first. With an `mfe.json` `budgets.initialJsKb` the entry plus the
15
+ * Judges the JS a remote loads first. With a `mfeflow.app.json` `budgets.initialJsKb` the entry plus the
16
16
  * primary expose must fit it; without one the entry alone must fit the cost model's `remoteKb`.
17
17
  */
18
18
  export declare function judgeRemoteInitial(bytes: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mfeflow/contracts",
3
- "version": "0.5.0",
4
- "description": "Types, validators and JSON Schemas for mfeflow: mfe.json ownership manifests, mfeflow.json project state, and the remote registry (entries and index).",
3
+ "version": "0.7.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",