mfeflow 0.3.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/README.md +17 -14
- package/dist/commands/features.d.ts +2 -2
- package/dist/commands/registry.d.ts +52 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +323 -33
- package/dist/output.d.ts +43 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -13,20 +13,23 @@ pnpm dev # shell on :4300, remotes on :4311,
|
|
|
13
13
|
|
|
14
14
|
## Commands
|
|
15
15
|
|
|
16
|
-
| Command | Does
|
|
17
|
-
| ------------------------------------- |
|
|
18
|
-
| `mfeflow init [dir]` | New project: a shell, remotes (default `catalog`, `cart`), pnpm workspace, Turborepo
|
|
19
|
-
| `mfeflow add remote <name>` | New remote with its registry entries and a route in every shell
|
|
20
|
-
| `mfeflow add shell <name>` | New shell that routes to every existing remote
|
|
21
|
-
| `mfeflow add <feature...>` | Add features, or `@namespace/feature` from a registry
|
|
22
|
-
| `mfeflow add env-key <KEY>` | Add a typed env key: `--scope server` (env.ts) or `runtime`, with `--schema <expr>`
|
|
23
|
-
| `mfeflow features [search]` | List the features you can add
|
|
24
|
-
| `mfeflow
|
|
25
|
-
| `mfeflow
|
|
26
|
-
| `mfeflow
|
|
27
|
-
| `mfeflow
|
|
28
|
-
| `mfeflow
|
|
29
|
-
| `mfeflow
|
|
16
|
+
| Command | Does |
|
|
17
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
18
|
+
| `mfeflow init [dir]` | New project: a shell, remotes (default `catalog`, `cart`), pnpm workspace, Turborepo |
|
|
19
|
+
| `mfeflow add remote <name>` | New remote with its registry entries and a route in every shell |
|
|
20
|
+
| `mfeflow add shell <name>` | New shell that routes to every existing remote |
|
|
21
|
+
| `mfeflow add <feature...>` | Add features, or `@namespace/feature` from a registry you added with `registry add` |
|
|
22
|
+
| `mfeflow add env-key <KEY>` | Add a typed env key: `--scope server` (env.ts) or `runtime`, with `--schema <expr>` |
|
|
23
|
+
| `mfeflow features [search]` | List and search the features you can add, from mfeflow and every configured registry |
|
|
24
|
+
| `mfeflow registry add @ns <url>` | Add a third-party feature registry (`https`, or `http` on loopback) to `mfeflow.json` |
|
|
25
|
+
| `mfeflow registry list`, `remove @ns` | List the registries, or remove one |
|
|
26
|
+
| `mfeflow registry build <dir>` | Check a folder of features and write `index.json` and the files to host |
|
|
27
|
+
| `mfeflow dev` | Run the shell and every remote, one prefixed log each; follows `add remote` |
|
|
28
|
+
| `mfeflow doctor` | Singletons, compatibility, config health, drift; `--fix` repairs the safe ones |
|
|
29
|
+
| `mfeflow validate` | `mfeflow.json`, every `mfe.json` and every registry entry against their schemas |
|
|
30
|
+
| `mfeflow check-compat` | Each remote's shared ranges against the shells it runs in |
|
|
31
|
+
| `mfeflow budget` | Built first-load JS of the shell and each remote against the cost-model budget |
|
|
32
|
+
| `mfeflow --version`, `mfeflow --help` | Version, and help with examples (`mfeflow <command> --help` for one command) |
|
|
30
33
|
|
|
31
34
|
Features that ship with it: `core` and `tsconfig` (always), `lint-oxlint`, `url-factory`, `env-t3` and `testing`
|
|
32
35
|
(default), `ci-github` and `git-hooks` (opt-in). `init` takes `--scope`, `--remotes a,b`, `--features a,b` and
|
|
@@ -46,8 +46,8 @@ export declare const FEATURES_ARGS: {
|
|
|
46
46
|
readonly search: {
|
|
47
47
|
readonly type: 'positional';
|
|
48
48
|
readonly required: false;
|
|
49
|
-
readonly description: 'Only features whose name or
|
|
49
|
+
readonly description: 'Only features whose name, description or keywords contain this';
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
|
-
export declare function matches(feature: FeatureInfo, search: string): boolean;
|
|
52
|
+
export declare function matches(feature: FeatureInfo, search: string, keywords?: string[]): boolean;
|
|
53
53
|
export declare function runFeatures(rt: Runtime, args: Record<string, unknown>, raw: readonly string[]): Promise<FeaturesResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { RegistryResult } from '../output.js';
|
|
2
|
+
import type { Runtime } from '../runtime.js';
|
|
3
|
+
export declare const REGISTRY_ARGS: {
|
|
4
|
+
readonly yes: {
|
|
5
|
+
readonly type: 'boolean';
|
|
6
|
+
readonly alias: 'y';
|
|
7
|
+
readonly description: 'Take the defaults and never prompt';
|
|
8
|
+
};
|
|
9
|
+
readonly preset: {
|
|
10
|
+
readonly type: 'string';
|
|
11
|
+
readonly description: 'Replay options from a JSON file or an https URL';
|
|
12
|
+
readonly valueHint: 'file|url';
|
|
13
|
+
};
|
|
14
|
+
readonly json: {
|
|
15
|
+
readonly type: 'boolean';
|
|
16
|
+
readonly description: 'Print one JSON document on stdout, nothing else';
|
|
17
|
+
};
|
|
18
|
+
readonly 'dry-run': {
|
|
19
|
+
readonly type: 'boolean';
|
|
20
|
+
readonly description: 'Print the summary and diff, write nothing';
|
|
21
|
+
};
|
|
22
|
+
readonly cwd: {
|
|
23
|
+
readonly type: 'string';
|
|
24
|
+
readonly description: 'Run as if started in this folder';
|
|
25
|
+
readonly valueHint: 'dir';
|
|
26
|
+
};
|
|
27
|
+
readonly install: {
|
|
28
|
+
readonly type: 'boolean';
|
|
29
|
+
readonly default: true;
|
|
30
|
+
readonly description: 'Run the package manager after writing (--no-install to skip)';
|
|
31
|
+
};
|
|
32
|
+
readonly verbose: {
|
|
33
|
+
readonly type: 'boolean';
|
|
34
|
+
readonly description: 'Show package manager output and stack traces';
|
|
35
|
+
};
|
|
36
|
+
readonly local: {
|
|
37
|
+
readonly type: 'string';
|
|
38
|
+
readonly description: 'Link @mfeflow/* from a built mfeflow checkout (env MFEFLOW_LOCAL)';
|
|
39
|
+
readonly valueHint: 'path';
|
|
40
|
+
};
|
|
41
|
+
readonly help: {
|
|
42
|
+
readonly type: 'boolean';
|
|
43
|
+
readonly alias: 'h';
|
|
44
|
+
readonly description: 'Show this help';
|
|
45
|
+
};
|
|
46
|
+
readonly out: {
|
|
47
|
+
readonly type: 'string';
|
|
48
|
+
readonly description: 'registry build: the folder to write (default: <dir>/dist)';
|
|
49
|
+
readonly valueHint: 'dir';
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export declare function runRegistry(rt: Runtime, args: Record<string, unknown>, raw: readonly string[]): Promise<RegistryResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { main } from './cli.js';
|
|
|
2
2
|
export { COMMANDS } from './cli.js';
|
|
3
3
|
export { EXIT, exitCodeFor } from './exit.js';
|
|
4
4
|
export { validateResult } from './output.js';
|
|
5
|
-
export type { AddResult, BudgetResult, DevResult, CliResult, FailureResult, FeatureInfo, FeaturesResult, InitResult, PlanReport, ReportResult, } from './output.js';
|
|
5
|
+
export type { AddResult, BudgetResult, DevResult, CliResult, FailureResult, FeatureInfo, FeaturesResult, InitResult, PlanReport, RegistryEntryInfo, RegistryResult, RegistryStatus, ReportResult, } from './output.js';
|
|
6
6
|
export { realRuntime, type Runtime } from './runtime.js';
|
|
7
7
|
export type { Io } from './io.js';
|
|
8
8
|
export type { Prompter } from './prompter.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
-
import { MfeflowError, PlanBuilder, applyPlan, dotenvKeys, findDiverged, findProjectRoot, getHttps, hasErrno, inOrder, isMfeflowError, loadFeature, loadProject, messageOf, parseFeature, readCappedText, realFs, recordPlan, renderDiff, renderTemplate, resolveFeature, runtimeConfigFiles, schemaKeys, serializeProject, summarize } from "@mfeflow/core";
|
|
2
|
+
import { MfeflowError, PlanBuilder, applyPlan, buildRegistry, dotenvKeys, fetchFeatureIndex, findDiverged, findProjectRoot, getHttps, hasErrno, inOrder, isMfeflowError, loadFeature, loadProject, messageOf, parseFeature, readCappedText, realFs, recordPlan, renderDiff, renderTemplate, resolveFeature, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, summarize, writeFileAtomic } from "@mfeflow/core";
|
|
3
3
|
import { defineCommand, runCommand } from "citty";
|
|
4
4
|
import node_path from "node:path";
|
|
5
|
-
import { COST_MODEL, DOCS_URL, MFE_NAME, assetsFromManifest, isSafeRelativePath, judgeCost, round1, validateMfeManifest, validateRegistryEntry } from "@mfeflow/contracts";
|
|
5
|
+
import { COST_MODEL, DOCS_URL, MFE_NAME, REGISTRY_NAMESPACE, assetsFromManifest, isSafeRelativePath, judgeCost, registryIndexUrl, registryTemplateProblem, round1, validateMfeManifest, validateRegistryEntry } from "@mfeflow/contracts";
|
|
6
6
|
import promises from "node:fs/promises";
|
|
7
7
|
import node_fs from "node:fs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
@@ -765,6 +765,7 @@ function validateResult(value) {
|
|
|
765
765
|
if (null !== value.search && 'string' != typeof value.search) c.add('search', 'must be text or null');
|
|
766
766
|
c.objects(value, 'features', '', (f, p)=>{
|
|
767
767
|
for (const key of [
|
|
768
|
+
'source',
|
|
768
769
|
'name',
|
|
769
770
|
'version',
|
|
770
771
|
"description"
|
|
@@ -775,6 +776,51 @@ function validateResult(value) {
|
|
|
775
776
|
'installed'
|
|
776
777
|
])c.boolean(f, key, p);
|
|
777
778
|
});
|
|
779
|
+
c.objects(value, 'registries', '', (r, p)=>{
|
|
780
|
+
for (const key of [
|
|
781
|
+
'namespace',
|
|
782
|
+
'index'
|
|
783
|
+
])c.string(r, key, p);
|
|
784
|
+
if ('number' != typeof r.items) c.add(`${p}items`, 'must be a number');
|
|
785
|
+
if (null !== r.error) if (isObj(r.error)) for (const key of [
|
|
786
|
+
'code',
|
|
787
|
+
'message',
|
|
788
|
+
'fix',
|
|
789
|
+
'docs'
|
|
790
|
+
])c.string(r.error, key, `${p}error.`);
|
|
791
|
+
else c.add(`${p}error`, 'must be an object or null');
|
|
792
|
+
});
|
|
793
|
+
break;
|
|
794
|
+
case 'registry':
|
|
795
|
+
if (![
|
|
796
|
+
'add',
|
|
797
|
+
'remove',
|
|
798
|
+
'list',
|
|
799
|
+
'build'
|
|
800
|
+
].includes(String(value.action))) c.add('action', 'must be add, remove, list or build');
|
|
801
|
+
c.boolean(value, 'dryRun', '');
|
|
802
|
+
c.boolean(value, 'changed', '');
|
|
803
|
+
if (null !== value.root) c.string(value, 'root', '');
|
|
804
|
+
c.objects(value, 'registries', '', (r, p)=>{
|
|
805
|
+
for (const key of [
|
|
806
|
+
'namespace',
|
|
807
|
+
'url',
|
|
808
|
+
'index'
|
|
809
|
+
])c.string(r, key, p);
|
|
810
|
+
});
|
|
811
|
+
if (null !== value.build) if (isObj(value.build)) {
|
|
812
|
+
for (const key of [
|
|
813
|
+
'namespace',
|
|
814
|
+
'out'
|
|
815
|
+
])c.string(value.build, key, 'build.');
|
|
816
|
+
c.texts(value.build, 'files', 'build.');
|
|
817
|
+
c.objects(value.build, 'features', 'build.', (f, p)=>{
|
|
818
|
+
for (const key of [
|
|
819
|
+
'name',
|
|
820
|
+
'version'
|
|
821
|
+
])c.string(f, key, p);
|
|
822
|
+
});
|
|
823
|
+
} else c.add('build', 'must be an object or null');
|
|
778
824
|
break;
|
|
779
825
|
case 'doctor':
|
|
780
826
|
case 'validate':
|
|
@@ -797,7 +843,7 @@ function validateResult(value) {
|
|
|
797
843
|
if (!isObj(value.overrides)) c.add('overrides', 'must be an object');
|
|
798
844
|
break;
|
|
799
845
|
default:
|
|
800
|
-
c.add('command', 'must be init, add, features, doctor, validate, check-compat, budget or dev');
|
|
846
|
+
c.add('command', 'must be init, add, features, registry, doctor, validate, check-compat, budget or dev');
|
|
801
847
|
}
|
|
802
848
|
return c.issues;
|
|
803
849
|
}
|
|
@@ -3483,60 +3529,121 @@ const FEATURES_ARGS = {
|
|
|
3483
3529
|
search: {
|
|
3484
3530
|
type: 'positional',
|
|
3485
3531
|
required: false,
|
|
3486
|
-
description: "Only features whose name or
|
|
3532
|
+
description: "Only features whose name, description or keywords contain this"
|
|
3487
3533
|
},
|
|
3488
3534
|
...GLOBAL_ARGS
|
|
3489
3535
|
};
|
|
3490
|
-
async function
|
|
3536
|
+
async function projectAt(cwd) {
|
|
3491
3537
|
try {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3538
|
+
return await loadProject(await findProjectRoot(cwd));
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
if (isMfeflowError(error) && 'MFEFLOW_E102' === error.code) return;
|
|
3541
|
+
throw error;
|
|
3496
3542
|
}
|
|
3497
3543
|
}
|
|
3498
|
-
function matches(feature, search) {
|
|
3544
|
+
function matches(feature, search, keywords = []) {
|
|
3499
3545
|
const needle = search.toLowerCase();
|
|
3500
|
-
return feature.name.toLowerCase().includes(needle) || feature.description.toLowerCase().includes(needle);
|
|
3546
|
+
return feature.name.toLowerCase().includes(needle) || feature.description.toLowerCase().includes(needle) || keywords.some((keyword)=>keyword.toLowerCase().includes(needle));
|
|
3501
3547
|
}
|
|
3502
|
-
function features_line(feature) {
|
|
3548
|
+
function features_line(feature, widths) {
|
|
3503
3549
|
const tags = [
|
|
3504
|
-
feature.always ? 'always' : feature.default ? 'default' : 'opt-in',
|
|
3550
|
+
feature.always ? 'always' : 'mfeflow' === feature.source ? feature.default ? 'default' : 'opt-in' : 'registry',
|
|
3505
3551
|
...feature.installed ? [
|
|
3506
3552
|
'installed'
|
|
3507
3553
|
] : []
|
|
3508
3554
|
];
|
|
3509
|
-
return ` ${feature.name.padEnd(
|
|
3555
|
+
return ` ${feature.source.padEnd(widths.source)} ${feature.name.padEnd(widths.name)} ${feature.version.padEnd(7)} ${feature.description} [${tags.join(', ')}]`;
|
|
3556
|
+
}
|
|
3557
|
+
function fetchIndexes(rt, registries) {
|
|
3558
|
+
return Promise.all(Object.entries(registries).toSorted(([a], [b])=>a.localeCompare(b)).map(async ([namespace, template])=>{
|
|
3559
|
+
const base = {
|
|
3560
|
+
namespace,
|
|
3561
|
+
index: registryIndexUrl(template)
|
|
3562
|
+
};
|
|
3563
|
+
try {
|
|
3564
|
+
const items = await fetchFeatureIndex(namespace, registries, rt.fetch);
|
|
3565
|
+
return {
|
|
3566
|
+
status: {
|
|
3567
|
+
...base,
|
|
3568
|
+
items: items.length,
|
|
3569
|
+
error: null
|
|
3570
|
+
},
|
|
3571
|
+
items
|
|
3572
|
+
};
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
const report = isMfeflowError(error) ? reportOf(error) : unexpectedReport(error);
|
|
3575
|
+
return {
|
|
3576
|
+
status: {
|
|
3577
|
+
...base,
|
|
3578
|
+
items: 0,
|
|
3579
|
+
error: report
|
|
3580
|
+
},
|
|
3581
|
+
items: []
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
}));
|
|
3510
3585
|
}
|
|
3511
3586
|
async function runFeatures(rt, args, raw) {
|
|
3512
3587
|
assertKnownFlags(raw, FEATURES_ARGS);
|
|
3513
3588
|
const globals = readGlobals(args, rt.io.env, rt.io.cwd);
|
|
3514
3589
|
const search = 'string' == typeof args.search && '' !== args.search ? args.search : null;
|
|
3515
|
-
const
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3590
|
+
const project = await projectAt(globals.cwd);
|
|
3591
|
+
const installed = new Set(Object.keys(project?.features ?? {}));
|
|
3592
|
+
const [firstParty, fetched] = await Promise.all([
|
|
3593
|
+
Promise.all(FIRST_PARTY.map(async (first)=>{
|
|
3594
|
+
const { manifest } = await loadAny(first.name, {
|
|
3595
|
+
roots: rt.roots
|
|
3596
|
+
});
|
|
3597
|
+
return {
|
|
3598
|
+
source: 'mfeflow',
|
|
3599
|
+
name: first.name,
|
|
3600
|
+
version: manifest.version,
|
|
3601
|
+
description: manifest.description ?? '',
|
|
3602
|
+
always: first.always,
|
|
3603
|
+
default: first.default,
|
|
3604
|
+
installed: installed.has(first.name)
|
|
3605
|
+
};
|
|
3606
|
+
})),
|
|
3607
|
+
fetchIndexes(rt, project?.registries ?? {})
|
|
3608
|
+
]);
|
|
3609
|
+
const fromRegistries = fetched.flatMap(({ status, items })=>items.toSorted((a, b)=>a.name.localeCompare(b.name)).map((item)=>{
|
|
3610
|
+
const name = `${status.namespace}/${item.name}`;
|
|
3611
|
+
return [
|
|
3612
|
+
{
|
|
3613
|
+
source: status.namespace,
|
|
3614
|
+
name,
|
|
3615
|
+
version: item.version,
|
|
3616
|
+
description: item.description,
|
|
3617
|
+
always: false,
|
|
3618
|
+
default: false,
|
|
3619
|
+
installed: installed.has(name)
|
|
3620
|
+
},
|
|
3621
|
+
item.keywords ?? []
|
|
3622
|
+
];
|
|
3623
|
+
}));
|
|
3624
|
+
const features = [
|
|
3625
|
+
...firstParty.filter((f)=>null === search || matches(f, search)),
|
|
3626
|
+
...fromRegistries.filter(([f, keywords])=>null === search || matches(f, search, keywords)).map(([f])=>f)
|
|
3627
|
+
];
|
|
3628
|
+
const registries = fetched.map((f)=>f.status);
|
|
3530
3629
|
if (!globals.json) {
|
|
3531
3630
|
if (0 === features.length) rt.io.stdout(`No features match "${search ?? ''}".`);
|
|
3532
|
-
else
|
|
3533
|
-
|
|
3631
|
+
else {
|
|
3632
|
+
const widths = {
|
|
3633
|
+
source: Math.max(...features.map((f)=>f.source.length)),
|
|
3634
|
+
name: Math.max(14, ...features.map((f)=>f.name.length))
|
|
3635
|
+
};
|
|
3636
|
+
rt.io.stdout(features.map((f)=>features_line(f, widths)).join('\n'));
|
|
3637
|
+
}
|
|
3638
|
+
for (const { namespace, error } of registries)if (error) rt.io.stderr(`Could not read ${namespace}: ${error.message}\n ${error.fix}`);
|
|
3639
|
+
rt.io.stdout(0 === registries.length ? '\nFrom a registry: mfeflow registry add @namespace <url>, then mfeflow add @namespace/name.' : '\nAdd one with: mfeflow add @namespace/name.');
|
|
3534
3640
|
}
|
|
3535
3641
|
return {
|
|
3536
3642
|
ok: true,
|
|
3537
3643
|
command: 'features',
|
|
3538
3644
|
search,
|
|
3539
|
-
features
|
|
3645
|
+
features,
|
|
3646
|
+
registries
|
|
3540
3647
|
};
|
|
3541
3648
|
}
|
|
3542
3649
|
const INIT_ARGS = {
|
|
@@ -3789,6 +3896,175 @@ function printInit(rt, result) {
|
|
|
3789
3896
|
rt.io.stdout(`\nCreated ${result.root} with a shell and ${String(remotes.length)} remote${1 === remotes.length ? '' : 's'} (${remotes.join(', ')}).`);
|
|
3790
3897
|
rt.io.stdout(`Next:\n${result.next.map((line)=>` ${line}`).join('\n')}`);
|
|
3791
3898
|
}
|
|
3899
|
+
const REGISTRY_ARGS = {
|
|
3900
|
+
out: {
|
|
3901
|
+
type: 'string',
|
|
3902
|
+
description: 'registry build: the folder to write (default: <dir>/dist)',
|
|
3903
|
+
valueHint: 'dir'
|
|
3904
|
+
},
|
|
3905
|
+
...GLOBAL_ARGS
|
|
3906
|
+
};
|
|
3907
|
+
const USAGE = 'mfeflow registry add @namespace <url-template> | list | remove @namespace | build <dir>';
|
|
3908
|
+
function registry_entries(project) {
|
|
3909
|
+
return Object.entries(project.registries ?? {}).toSorted(([a], [b])=>a.localeCompare(b)).map(([namespace, url])=>({
|
|
3910
|
+
namespace,
|
|
3911
|
+
url,
|
|
3912
|
+
index: registryIndexUrl(url)
|
|
3913
|
+
}));
|
|
3914
|
+
}
|
|
3915
|
+
function registry_result(action, globals, extra) {
|
|
3916
|
+
return {
|
|
3917
|
+
ok: true,
|
|
3918
|
+
command: 'registry',
|
|
3919
|
+
action,
|
|
3920
|
+
dryRun: globals.dryRun,
|
|
3921
|
+
root: null,
|
|
3922
|
+
changed: false,
|
|
3923
|
+
build: null,
|
|
3924
|
+
...extra
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
function assertNamespace(namespace, verb) {
|
|
3928
|
+
if (void 0 === namespace) throw new MfeflowError('E107', `registry ${verb} needs a namespace like @someone`);
|
|
3929
|
+
if ('@mfeflow' === namespace) throw new MfeflowError('E107', '@mfeflow is reserved for the features that ship with mfeflow');
|
|
3930
|
+
if (!REGISTRY_NAMESPACE.test(namespace)) throw new MfeflowError('E107', `"${namespace}" is not a namespace; they start with @ and use lower-case letters, digits and "-" (for example @someone)`);
|
|
3931
|
+
return namespace;
|
|
3932
|
+
}
|
|
3933
|
+
async function projectOf(globals) {
|
|
3934
|
+
const root = await findProjectRoot(globals.cwd);
|
|
3935
|
+
return {
|
|
3936
|
+
root,
|
|
3937
|
+
project: await loadProject(root)
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
async function save(globals, root, project, registries) {
|
|
3941
|
+
const { registries: _old, ...rest } = project;
|
|
3942
|
+
const next = Object.keys(registries).length > 0 ? {
|
|
3943
|
+
...rest,
|
|
3944
|
+
registries
|
|
3945
|
+
} : rest;
|
|
3946
|
+
if (!globals.dryRun) await saveProject(root, next);
|
|
3947
|
+
return next;
|
|
3948
|
+
}
|
|
3949
|
+
async function addRegistry(rt, globals, namespaceArg, url) {
|
|
3950
|
+
const namespace = assertNamespace(namespaceArg, 'add');
|
|
3951
|
+
if (void 0 === url) throw new MfeflowError('E107', `registry add needs a URL: mfeflow registry add ${namespace} https://example.com/{name}.json`);
|
|
3952
|
+
const problem = registryTemplateProblem(url);
|
|
3953
|
+
if (void 0 !== problem) throw new MfeflowError('E107', `the URL ${problem}`);
|
|
3954
|
+
const { root, project } = await projectOf(globals);
|
|
3955
|
+
const current = project.registries ?? {};
|
|
3956
|
+
const existing = current[namespace];
|
|
3957
|
+
if (existing === url) {
|
|
3958
|
+
if (!globals.json) rt.io.stdout(`${namespace} already points at ${url}; nothing to do.`);
|
|
3959
|
+
return registry_result('add', globals, {
|
|
3960
|
+
root,
|
|
3961
|
+
registries: registry_entries(project)
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
if (void 0 !== existing) throw new MfeflowError('E107', `${namespace} already points at ${existing}`, {
|
|
3965
|
+
fix: `Run \`mfeflow registry remove ${namespace}\` first, then add it again.`
|
|
3966
|
+
});
|
|
3967
|
+
const next = await save(globals, root, project, {
|
|
3968
|
+
...current,
|
|
3969
|
+
[namespace]: url
|
|
3970
|
+
});
|
|
3971
|
+
if (!globals.json) rt.io.stdout(globals.dryRun ? `Dry run: would add ${namespace} -> ${url}. Nothing was written.` : `Added ${namespace} -> ${url}\nSee what it offers: mfeflow features`);
|
|
3972
|
+
return registry_result('add', globals, {
|
|
3973
|
+
root,
|
|
3974
|
+
changed: true,
|
|
3975
|
+
registries: registry_entries(next)
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
async function removeRegistry(rt, globals, namespaceArg) {
|
|
3979
|
+
const namespace = assertNamespace(namespaceArg, 'remove');
|
|
3980
|
+
const { root, project } = await projectOf(globals);
|
|
3981
|
+
const { [namespace]: url, ...rest } = project.registries ?? {};
|
|
3982
|
+
if (void 0 === url) throw new MfeflowError('E107', `${namespace} is not in this project's registries`, {
|
|
3983
|
+
fix: 'List them with `mfeflow registry list`.'
|
|
3984
|
+
});
|
|
3985
|
+
const using = Object.keys(project.features).filter((name)=>name.startsWith(`${namespace}/`));
|
|
3986
|
+
if (using.length > 0) throw new MfeflowError('E107', `${using.join(', ')} installed from ${namespace}`, {
|
|
3987
|
+
fix: `Keep ${namespace} while they are installed: \`mfeflow add remote\` and \`add shell\` fetch them again and fail without it.`
|
|
3988
|
+
});
|
|
3989
|
+
const next = await save(globals, root, project, rest);
|
|
3990
|
+
if (!globals.json) rt.io.stdout(globals.dryRun ? `Dry run: would remove ${namespace}. Nothing was written.` : `Removed ${namespace} (${url}).`);
|
|
3991
|
+
return registry_result('remove', globals, {
|
|
3992
|
+
root,
|
|
3993
|
+
changed: true,
|
|
3994
|
+
registries: registry_entries(next)
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
async function listRegistries(rt, globals) {
|
|
3998
|
+
const { root, project } = await projectOf(globals);
|
|
3999
|
+
const registries = registry_entries(project);
|
|
4000
|
+
if (!globals.json) if (0 === registries.length) rt.io.stdout('No registries. Add one: mfeflow registry add @namespace https://example.com/{name}.json');
|
|
4001
|
+
else {
|
|
4002
|
+
const width = Math.max(...registries.map((r)=>r.namespace.length));
|
|
4003
|
+
rt.io.stdout(registries.map((r)=>` ${r.namespace.padEnd(width)} ${r.url}`).join('\n'));
|
|
4004
|
+
}
|
|
4005
|
+
return registry_result('list', globals, {
|
|
4006
|
+
root,
|
|
4007
|
+
registries
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
async function buildFolder(rt, globals, args, dirArg) {
|
|
4011
|
+
if (void 0 === dirArg) throw new MfeflowError('E107', 'registry build needs the folder that holds your features: mfeflow registry build ./my-registry');
|
|
4012
|
+
const dir = node_path.resolve(globals.cwd, dirArg);
|
|
4013
|
+
const out = node_path.resolve(globals.cwd, args_text(args, 'out') ?? node_path.join(dirArg, 'dist'));
|
|
4014
|
+
const built = await buildRegistry(dir);
|
|
4015
|
+
if (!globals.dryRun) await inOrder(built.files, (file)=>writeFileAtomic(realFs, node_path.join(out, ...file.path.split('/')), file.content));
|
|
4016
|
+
if (!globals.json) {
|
|
4017
|
+
const lines = built.features.map((f)=>` ${f.name} ${f.version}`);
|
|
4018
|
+
rt.io.stdout([
|
|
4019
|
+
`${globals.dryRun ? 'Dry run: would build' : 'Built'} ${String(built.features.length)} feature(s) for ${built.namespace}:`,
|
|
4020
|
+
...lines,
|
|
4021
|
+
'',
|
|
4022
|
+
`${globals.dryRun ? 'Would write' : 'Wrote'} ${String(built.files.length)} files to ${out}`,
|
|
4023
|
+
'',
|
|
4024
|
+
"Host that folder on any static host (GitHub Pages works). Users then run:",
|
|
4025
|
+
` mfeflow registry add ${built.namespace} https://<your-host>/{name}.json`
|
|
4026
|
+
].join('\n'));
|
|
4027
|
+
}
|
|
4028
|
+
return registry_result('build', globals, {
|
|
4029
|
+
registries: [],
|
|
4030
|
+
build: {
|
|
4031
|
+
namespace: built.namespace,
|
|
4032
|
+
out,
|
|
4033
|
+
features: built.features.map(({ name, version })=>({
|
|
4034
|
+
name,
|
|
4035
|
+
version
|
|
4036
|
+
})),
|
|
4037
|
+
files: built.files.map((f)=>f.path)
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
async function runRegistry(rt, args, raw) {
|
|
4042
|
+
assertKnownFlags(raw, REGISTRY_ARGS);
|
|
4043
|
+
const globals = readGlobals(args, rt.io.env, rt.io.cwd);
|
|
4044
|
+
const positional = Array.isArray(args._) ? args._.map(String) : [];
|
|
4045
|
+
const [action, first, second] = positional;
|
|
4046
|
+
const takes = {
|
|
4047
|
+
add: 2,
|
|
4048
|
+
remove: 1,
|
|
4049
|
+
list: 0,
|
|
4050
|
+
build: 1
|
|
4051
|
+
};
|
|
4052
|
+
const surplus = positional[(takes[action ?? ''] ?? 1 / 0) + 1];
|
|
4053
|
+
if (void 0 !== surplus) throw new MfeflowError('E107', `unexpected "${surplus}" (${USAGE})`);
|
|
4054
|
+
switch(action){
|
|
4055
|
+
case 'add':
|
|
4056
|
+
return addRegistry(rt, globals, first, second);
|
|
4057
|
+
case 'remove':
|
|
4058
|
+
return removeRegistry(rt, globals, first);
|
|
4059
|
+
case 'list':
|
|
4060
|
+
return listRegistries(rt, globals);
|
|
4061
|
+
case 'build':
|
|
4062
|
+
return buildFolder(rt, globals, args, first);
|
|
4063
|
+
case void 0:
|
|
4064
|
+
default:
|
|
4065
|
+
throw new MfeflowError('E107', void 0 === action ? `registry needs an action (${USAGE})` : `unknown action "${action}" (${USAGE})`);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
3792
4068
|
const REPO_URL = 'https://github.com/mfeflow/mfeflow';
|
|
3793
4069
|
function flagLabel(name, def) {
|
|
3794
4070
|
const alias = 'alias' in def ? [
|
|
@@ -4062,7 +4338,7 @@ const COMMANDS = [
|
|
|
4062
4338
|
},
|
|
4063
4339
|
{
|
|
4064
4340
|
name: 'features',
|
|
4065
|
-
summary: 'list the features you can add',
|
|
4341
|
+
summary: 'list and search the features you can add, from mfeflow and your registries',
|
|
4066
4342
|
usage: 'mfeflow features [search] [options]',
|
|
4067
4343
|
args: FEATURES_ARGS,
|
|
4068
4344
|
examples: [
|
|
@@ -4072,6 +4348,20 @@ const COMMANDS = [
|
|
|
4072
4348
|
],
|
|
4073
4349
|
run: runFeatures
|
|
4074
4350
|
},
|
|
4351
|
+
{
|
|
4352
|
+
name: 'registry',
|
|
4353
|
+
summary: 'configure third-party feature registries, or build one to host',
|
|
4354
|
+
usage: 'mfeflow registry add @namespace <url-template> | mfeflow registry list | mfeflow registry remove @namespace | mfeflow registry build <dir> [--out dir]',
|
|
4355
|
+
args: REGISTRY_ARGS,
|
|
4356
|
+
examples: [
|
|
4357
|
+
'mfeflow registry add @acme https://acme.github.io/features/{name}.json',
|
|
4358
|
+
'mfeflow registry add @test http://127.0.0.1:4010/{name}.json # plain http: loopback hosts only',
|
|
4359
|
+
'mfeflow registry list --json',
|
|
4360
|
+
'mfeflow registry remove @acme --dry-run',
|
|
4361
|
+
'mfeflow registry build ./my-registry --out ./site'
|
|
4362
|
+
],
|
|
4363
|
+
run: runRegistry
|
|
4364
|
+
},
|
|
4075
4365
|
{
|
|
4076
4366
|
name: 'doctor',
|
|
4077
4367
|
summary: 'check singletons, compatibility, config and drift; --fix repairs the safe ones',
|
package/dist/output.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export interface AddResult extends WriteReport {
|
|
|
57
57
|
skipped: string[];
|
|
58
58
|
}
|
|
59
59
|
export interface FeatureInfo {
|
|
60
|
+
/** `mfeflow` for the features that ship with the CLI, otherwise the registry's namespace, like `@test`. */
|
|
61
|
+
source: string;
|
|
62
|
+
/** What you pass to `mfeflow add`: `lint-oxlint`, or `@test/hello` for a registry's. */
|
|
60
63
|
name: string;
|
|
61
64
|
version: string;
|
|
62
65
|
description: string;
|
|
@@ -64,11 +67,50 @@ export interface FeatureInfo {
|
|
|
64
67
|
default: boolean;
|
|
65
68
|
installed: boolean;
|
|
66
69
|
}
|
|
70
|
+
export interface RegistryStatus {
|
|
71
|
+
namespace: string;
|
|
72
|
+
/** The index.json asked for. */
|
|
73
|
+
index: string;
|
|
74
|
+
/** How many items it lists. */
|
|
75
|
+
items: number;
|
|
76
|
+
/** Why it could not be read; the other registries are listed regardless. */
|
|
77
|
+
error: ErrorReport | null;
|
|
78
|
+
}
|
|
67
79
|
export interface FeaturesResult {
|
|
68
80
|
ok: true;
|
|
69
81
|
command: 'features';
|
|
70
82
|
search: string | null;
|
|
71
83
|
features: FeatureInfo[];
|
|
84
|
+
registries: RegistryStatus[];
|
|
85
|
+
}
|
|
86
|
+
export interface RegistryEntryInfo {
|
|
87
|
+
namespace: string;
|
|
88
|
+
/** The URL template from mfeflow.json. */
|
|
89
|
+
url: string;
|
|
90
|
+
/** Where its index.json is. */
|
|
91
|
+
index: string;
|
|
92
|
+
}
|
|
93
|
+
export interface RegistryResult {
|
|
94
|
+
ok: true;
|
|
95
|
+
command: 'registry';
|
|
96
|
+
action: 'add' | 'remove' | 'list' | 'build';
|
|
97
|
+
dryRun: boolean;
|
|
98
|
+
/** The project folder; null for `build`, which needs none. */
|
|
99
|
+
root: string | null;
|
|
100
|
+
/** Whether mfeflow.json changed (or would have, for a dry run). */
|
|
101
|
+
changed: boolean;
|
|
102
|
+
/** The registries of the project after the action. */
|
|
103
|
+
registries: RegistryEntryInfo[];
|
|
104
|
+
build: {
|
|
105
|
+
namespace: string;
|
|
106
|
+
out: string;
|
|
107
|
+
features: {
|
|
108
|
+
name: string;
|
|
109
|
+
version: string;
|
|
110
|
+
}[];
|
|
111
|
+
/** Relative to `out`. */
|
|
112
|
+
files: string[];
|
|
113
|
+
} | null;
|
|
72
114
|
}
|
|
73
115
|
/** What `doctor`, `validate` and `check-compat` print with `--json`. `ok` is false when any finding is an error. */
|
|
74
116
|
export interface ReportResult {
|
|
@@ -147,7 +189,7 @@ export interface FailureResult {
|
|
|
147
189
|
error: ErrorReport;
|
|
148
190
|
exitCode: number;
|
|
149
191
|
}
|
|
150
|
-
export type CliResult = InitResult | AddResult | FeaturesResult | ReportResult | BudgetResult | DevResult | FailureResult;
|
|
192
|
+
export type CliResult = InitResult | AddResult | FeaturesResult | RegistryResult | ReportResult | BudgetResult | DevResult | FailureResult;
|
|
151
193
|
/** The plan as plain data for `--json`, without file contents. Features are listed once. */
|
|
152
194
|
export declare function planReport(plan: Plan): PlanReport;
|
|
153
195
|
/** Checks a document the CLI printed with `--json`. Returns the problems; empty means valid. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mfeflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create and grow a React micro-frontend platform: mfeflow init, add remote, add <feature>.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@clack/prompts": "1.8.1",
|
|
52
|
-
"@mfeflow/contracts": "0.
|
|
53
|
-
"@mfeflow/core": "0.
|
|
52
|
+
"@mfeflow/contracts": "0.4.0",
|
|
53
|
+
"@mfeflow/core": "0.4.0",
|
|
54
54
|
"citty": "0.2.2",
|
|
55
55
|
"semver": "7.8.5",
|
|
56
56
|
"yaml": "2.9.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@mfeflow/rsbuild": "0.
|
|
59
|
+
"@mfeflow/rsbuild": "0.4.0",
|
|
60
60
|
"@rslib/core": "1.0.5",
|
|
61
61
|
"@types/node": "26.6.6",
|
|
62
62
|
"@types/semver": "7.8.0",
|