@oj-module/cli 0.2.30 → 0.2.43
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/devkit/manual.md +5 -5
- package/devkit/module-dev-and-cdn-playground.md +1 -1
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/cli.test.ts +23 -14
- package/src/__tests__/list.test.ts +176 -0
- package/src/__tests__/registry-cli.test.ts +522 -0
- package/src/__tests__/registry-export.test.ts +319 -0
- package/src/__tests__/registry-import.test.ts +336 -0
- package/src/__tests__/registry-sign.test.ts +352 -0
- package/src/cli-output.ts +81 -0
- package/src/cli.ts +298 -39
- package/src/config-cmd.ts +100 -0
- package/src/contract/__tests__/handler-scan.test.ts +74 -2
- package/src/contract/handler-scan.ts +27 -2
- package/src/enable.ts +1 -1
- package/src/exit-codes.ts +33 -0
- package/src/index.ts +5 -9
- package/src/install.ts +120 -38
- package/src/lifecycle/__tests__/authoring.test.ts +6 -5
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +5 -4
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +9 -6
- package/src/lifecycle/__tests__/helpers.ts +10 -6
- package/src/lifecycle/__tests__/install.test.ts +72 -15
- package/src/lifecycle/__tests__/manifest-contract.test.ts +40 -1
- package/src/lifecycle/__tests__/publish.test.ts +174 -33
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +9 -6
- package/src/lifecycle/__tests__/sign.test.ts +9 -8
- package/src/lifecycle/__tests__/smoke.test.ts +1 -1
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/lifecycle/sign.ts +8 -7
- package/src/list.ts +146 -0
- package/src/manifest-contract.ts +11 -5
- package/src/market/__tests__/bootstrap.test.ts +4 -1
- package/src/market/__tests__/build-index.test.ts +529 -42
- package/src/market/__tests__/fixture.test.ts +17 -1
- package/src/market/__tests__/publish.test.ts +8 -7
- package/src/market/__tests__/setup.test.ts +4 -4
- package/src/market/__tests__/sharding.test.ts +90 -0
- package/src/market/__tests__/verify.test.ts +237 -13
- package/src/market/__tests__/yank-ledger.test.ts +60 -0
- package/src/market/bootstrap.ts +8 -3
- package/src/market/build-index.ts +407 -40
- package/src/market/cdn-layout.ts +3 -2
- package/src/market/command.ts +40 -6
- package/src/market/failure-playbook.ts +1 -1
- package/src/market/fixture.ts +20 -3
- package/src/market/index-builder.ts +120 -47
- package/src/market/publish.ts +3 -1
- package/src/market/setup.ts +3 -3
- package/src/market/sharding.ts +94 -0
- package/src/market/trial.ts +1 -1
- package/src/market/verify.ts +220 -53
- package/src/market/walkthrough.ts +7 -6
- package/src/market/yank-ledger.ts +91 -0
- package/src/publish.ts +250 -38
- package/src/registry-export.ts +405 -0
- package/src/registry-import.ts +410 -0
- package/src/registry-sidecar.ts +84 -0
- package/src/registry-sign.ts +322 -0
- package/src/registry-source.ts +192 -0
- package/src/sign.ts +13 -10
- package/src/site.ts +4 -4
- package/src/trust.ts +1 -1
- package/src/uninstall.ts +14 -2
- package/src/usage.ts +130 -25
- package/src/view.ts +94 -0
- package/templates/cdn/README.md +35 -5
- package/templates/config.yaml +46 -96
- package/templates/init.include.yaml +1 -1
- package/templates/src/_platform/api/_lifecycle/atomic.ts +3 -0
- package/templates/src/_platform/api/_lifecycle/installer.ts +44 -7
- package/templates/src/_platform/api/_lifecycle/limits.ts +21 -0
- package/templates/src/_platform/api/_lifecycle/source-seq.ts +88 -0
- package/templates/src/_platform/api/_lifecycle/steps/download.ts +15 -0
- package/templates/src/_platform/api/_lifecycle/steps/extract.ts +61 -2
- package/templates/src/_platform/api/_lifecycle/steps/preflight.ts +55 -6
- package/templates/src/_platform/api/_lifecycle/steps/register.ts +2 -1
- package/templates/src/_platform/api/_lifecycle/steps/verify.ts +22 -8
- package/templates/src/_platform/api/_lifecycle/tgz-guard.ts +96 -0
- package/templates/src/_platform/api/_lifecycle/types.ts +6 -3
- package/templates/src/_platform/api/_lifecycle/verify-checks.ts +8 -0
- package/templates/src/_platform/api/_shared/service/domain/solution-perms.ts +9 -1
- package/templates/src/_platform/api/_shared/site-facade.ts +62 -11
- package/templates/src/_platform/api/manifest.yaml +1 -0
- package/templates/src/_platform/api/market/api.ts +362 -99
- package/templates/src/_platform/api/market/index-source.ts +112 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.mysql.sql +9 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.pg.sql +7 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.sql +8 -0
- package/templates/src/_platform/api/schema.yaml +8 -0
- package/templates/src/_platform/api/solutions/api.ts +8 -3
- package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +13 -0
- package/templates/src/oj-market/web/__tests__/InstallDialog.test.tsx +38 -0
- package/templates/src/oj-market/web/components/CardItem.tsx +10 -0
- package/templates/src/oj-market/web/components/InstallDialog.tsx +18 -0
- package/templates/src/oj-market/web/locales/en-US.json +1 -0
- package/templates/src/oj-market/web/locales/zh-CN.json +1 -0
- package/templates/src/oj-market/web/types.ts +3 -1
- package/templates/tests/lifecycle/installer.spec.ts +24 -0
- package/templates/tests/lifecycle/market-api.spec.ts +641 -85
- package/templates/tests/lifecycle/preflight.spec.ts +52 -0
- package/templates/tests/lifecycle/sig-fixtures.spec.ts +7 -4
- package/templates/tests/lifecycle/source-seq.spec.ts +55 -0
- package/templates/tests/lifecycle/steps-fs.spec.ts +166 -7
- package/templates/tests/lifecycle/tgz-guard.spec.ts +133 -1
- package/templates/tests/lifecycle/verify-checks.spec.ts +27 -13
- package/templates/tests/lifecycle/verify-report.spec.ts +3 -1
- package/templates/tests/rbac-solution-perms.spec.ts +41 -0
- package/vendor/host-versions.json +3 -3
package/src/market/fixture.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface FixturePackage {
|
|
|
23
23
|
export const DEFAULT_FIXTURE_NAME = "demo-solution";
|
|
24
24
|
export const DEFAULT_FIXTURE_VERSION = "0.1.0";
|
|
25
25
|
|
|
26
|
+
/** 方案名/版本的安全字符集(与 cdn 文件名、staging 目录名拼接,须杜绝路径穿越) */
|
|
27
|
+
const SAFE_SPEC = /^[a-z0-9][\w.-]*$/i;
|
|
28
|
+
|
|
29
|
+
/** fail-closed:name/version 仅允许字母数字 . _ -,且不可含 `..`,否则路径拼接会逃逸出 cdn/staging 目录 */
|
|
30
|
+
function assertSafeSpec(part: string, label: string): void {
|
|
31
|
+
if (part === "" || !SAFE_SPEC.test(part) || part.includes(".."))
|
|
32
|
+
throw new Error(`[ojm] --fixture 的 ${label} 含非法字符(仅允许字母数字 . _ -,且不可含 ..):${part}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/** 解析 `--fixture <name>@<version>`;缺省/缺段用默认值补齐 */
|
|
27
36
|
export function parseFixtureSpec(spec?: string): { name: string, version: string } {
|
|
28
37
|
if (spec === undefined || spec.trim() === "" || spec === "latest")
|
|
@@ -32,7 +41,11 @@ export function parseFixtureSpec(spec?: string): { name: string, version: string
|
|
|
32
41
|
return { name: spec.trim(), version: DEFAULT_FIXTURE_VERSION };
|
|
33
42
|
const name = spec.slice(0, at).trim();
|
|
34
43
|
const version = spec.slice(at + 1).trim();
|
|
35
|
-
|
|
44
|
+
const safeName = name === "" ? DEFAULT_FIXTURE_NAME : name;
|
|
45
|
+
const safeVersion = version === "" ? DEFAULT_FIXTURE_VERSION : version;
|
|
46
|
+
assertSafeSpec(safeName, "name");
|
|
47
|
+
assertSafeSpec(safeVersion, "version");
|
|
48
|
+
return { name: safeName, version: safeVersion };
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export function buildFixturePackage(opts: { stagingDir: string, name?: string, version?: string }): FixturePackage {
|
|
@@ -113,8 +126,12 @@ function cssClassOf(name: string, version: string): string {
|
|
|
113
126
|
return `${name}-v${version.replace(/[^0-9A-Z]/gi, "-")}`;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
129
|
+
/**
|
|
130
|
+
* 方案清单(Form A:archive 只写 file,sha256/size 由索引侧填 Form B)。
|
|
131
|
+
* 导出给 build-index 的体积实测用:需求 §7 估「单条 Form B ≈535–684B」偏乐观——本形态
|
|
132
|
+
* 实测 945B YAML、进索引 ≈1.2KB/条,SHARD_MAX_VERSIONS 的 1000 校准即以它为据。
|
|
133
|
+
*/
|
|
134
|
+
export function solutionManifest(name: string, version: string): Record<string, unknown> {
|
|
118
135
|
return {
|
|
119
136
|
name,
|
|
120
137
|
version,
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Form B 索引聚合(T4
|
|
2
|
+
* Form B 索引聚合(T4 + REG-CS5 三级化):把「一批已扫出的包」变成分片索引的数据形状。
|
|
3
3
|
*
|
|
4
|
-
* 纯数据变换,不落 IO、不签名——IO 与签名归 `build-index.ts
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 纯数据变换,不落 IO、不签名——IO 与签名归 `build-index.ts`(写入)与 `publish.ts`(补签)。
|
|
5
|
+
* 形状严格对齐安装侧的消费口径(`market/api.ts resolveCdn`):`archive.file` 必须逐字
|
|
6
|
+
* `<name>-<version>.tgz`,否则安装侧 P2-11 会判索引投毒直接 502。
|
|
7
|
+
*
|
|
8
|
+
* REG-CS5:v2 起索引三级化——`catalog.yaml`(包名 → 分片号,无版本明细)+ `shards/<n>.yaml`
|
|
9
|
+
* (每片 ≤1000 版本)+ 版本条目内嵌 Form B manifest(原 manifests.yaml 的聚合清单并入分片,
|
|
10
|
+
* 装一个包只拉 1 catalog + 1 shard,不再有第三份全量清单)。版本条目即 Form B 条目:
|
|
11
|
+
* `archive` 带 file/sha256/size/signature,与旧 manifests.yaml 逐字同构,故安装侧
|
|
12
|
+
* 「取版本条目即取 manifest」,少一次全量拉取。
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
import type { YankEntry } from "./yank-ledger";
|
|
16
|
+
|
|
17
|
+
/** 分片内的方案条目:版本数组 = Form B manifest 条目(升序) */
|
|
18
|
+
export interface BuiltSolution {
|
|
10
19
|
name: string
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
file: string
|
|
14
|
-
sha256: string
|
|
15
|
-
size: number
|
|
16
|
-
/** 包内 manifest(Form A),入 manifests.yaml 时补成 Form B */
|
|
17
|
-
manifest: Record<string, unknown>
|
|
18
|
-
/** 包级 detached 签名(`<tgz>.sig` 五字段);缺则 Form B 不带 signature,安装侧按 sig_missing 拒装 */
|
|
19
|
-
signature?: Record<string, unknown>
|
|
20
|
+
latest: string
|
|
21
|
+
versions: Array<Record<string, unknown>>
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface BuiltIndex {
|
|
23
|
-
|
|
24
|
-
manifests: Array<Record<string, unknown>>
|
|
25
|
+
solutions: BuiltSolution[]
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 聚合索引数据(分片形态)。
|
|
30
|
+
*
|
|
31
|
+
* `yanked?`:yank 账本(`name@version -> 条目`,REG-CS4)。非零时:
|
|
32
|
+
* - 各版本条目带 `yanked: true` + `yanked_at`(epoch 秒),安装侧 resolveCdn 据此拒精确安装;
|
|
33
|
+
* - 该方案的 `latest` 跳过已下线版本(全部下线则降级取最高版,保证索引不空)。
|
|
34
|
+
*/
|
|
35
|
+
export function buildIndexData(packages: IndexedPackage[], yanked?: Map<string, YankEntry>): BuiltIndex {
|
|
28
36
|
const byName = new Map<string, IndexedPackage[]>();
|
|
29
37
|
for (const pkg of packages) {
|
|
30
38
|
const list = byName.get(pkg.name) ?? [];
|
|
@@ -32,45 +40,110 @@ export function buildIndexData(packages: IndexedPackage[]): BuiltIndex {
|
|
|
32
40
|
byName.set(pkg.name, list);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
const solutions:
|
|
36
|
-
const manifests: Array<Record<string, unknown>> = [];
|
|
43
|
+
const solutions: BuiltSolution[] = [];
|
|
37
44
|
for (const name of [...byName.keys()].sort()) {
|
|
38
|
-
const list = [...byName.get(name)!].sort((a, b) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
manifests.
|
|
47
|
-
|
|
45
|
+
const list = [...byName.get(name)!].sort((a, b) => compareSemver(a.version, b.version));
|
|
46
|
+
// latest 跳过 yanked(REG-CS4):已下线的版本绝不成为默认升级目标;全下线降级取最高版
|
|
47
|
+
const live = list.filter(v => !isYanked(yanked, name, v.version));
|
|
48
|
+
const latestSource = live.length > 0 ? live : list;
|
|
49
|
+
const versions = list.map((v) => {
|
|
50
|
+
const key = yanked !== undefined && isYanked(yanked, name, v.version)
|
|
51
|
+
? { yanked: true, yanked_at: yanked.get(`${name}@${v.version}`)!.at }
|
|
52
|
+
: {};
|
|
53
|
+
// 版本条目 = Form B manifest 条目:name/version/archive 与旧 manifests.yaml 逐字同构,
|
|
54
|
+
// 顶层再并进版本裁决字段(yanked)。安装侧「取条目即取 manifest」。
|
|
55
|
+
return {
|
|
56
|
+
...v.manifest,
|
|
48
57
|
name,
|
|
49
|
-
version:
|
|
58
|
+
version: v.version,
|
|
50
59
|
archive: {
|
|
51
|
-
file
|
|
52
|
-
sha256:
|
|
53
|
-
size:
|
|
54
|
-
...(
|
|
60
|
+
file: `${v.name}-${v.version}.tgz`,
|
|
61
|
+
sha256: v.sha256,
|
|
62
|
+
size: v.size,
|
|
63
|
+
...(v.signature === undefined ? {} : { signature: v.signature }),
|
|
55
64
|
},
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
...key,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
solutions.push({ name, latest: latestSource[latestSource.length - 1]!.version, versions });
|
|
58
69
|
}
|
|
59
|
-
return {
|
|
70
|
+
return { solutions };
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
/**
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
/** 入索引的包(扫描产物;manifest 为包内 Form A,进分片时补成 Form B) */
|
|
74
|
+
export interface IndexedPackage {
|
|
75
|
+
name: string
|
|
76
|
+
version: string
|
|
77
|
+
/** tgz 文件名(由 name+version 派生,逐字约束在此处一次成型) */
|
|
78
|
+
file: string
|
|
79
|
+
sha256: string
|
|
80
|
+
size: number
|
|
81
|
+
/** 包内 manifest(Form A) */
|
|
82
|
+
manifest: Record<string, unknown>
|
|
83
|
+
/** 包级 detached 签名(`<tgz>.sig` 五字段);缺则 Form B 不带 signature,安装侧按 sig_missing 拒装 */
|
|
84
|
+
signature?: Record<string, unknown>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isYanked(yanked: Map<string, YankEntry> | undefined, name: string, version: string): boolean {
|
|
88
|
+
return yanked !== undefined && yanked.has(`${name}@${version}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* semver 比较(与模板 `_lifecycle/semver.ts` 同源算法;prerelease 遵循 semver 2.0.0):
|
|
93
|
+
* 数字标识符按数值、字母按字典序、数字 < 字母、标识符列表前缀相等时多者大;无 prerelease > 有 prerelease。
|
|
94
|
+
* 取代旧版纯数值比较(旧版对 `1.0.0-rc.1` 这类预发布版本按 `1.0.0.0` 处理,latest 计算失真——
|
|
95
|
+
* S2-0 红队 F 相关预发布序修复)。返回 -1|0|1,可直接作 Array.sort 比较器。
|
|
96
|
+
*/
|
|
97
|
+
export function compareSemver(a: string, b: string): -1 | 0 | 1 {
|
|
98
|
+
const va = splitVersion(a);
|
|
99
|
+
const vb = splitVersion(b);
|
|
100
|
+
const len = Math.max(va.core.length, vb.core.length);
|
|
101
|
+
for (let i = 0; i < len; i++) {
|
|
102
|
+
const diff = (va.core[i] ?? 0) - (vb.core[i] ?? 0);
|
|
68
103
|
if (diff !== 0)
|
|
69
|
-
return diff;
|
|
104
|
+
return diff < 0 ? -1 : 1;
|
|
70
105
|
}
|
|
71
|
-
return
|
|
106
|
+
return comparePrerelease(va.pre, vb.pre);
|
|
72
107
|
}
|
|
73
108
|
|
|
74
|
-
|
|
75
|
-
|
|
109
|
+
/** 兼容旧名(过渡期别名;新调用请用 compareSemver) */
|
|
110
|
+
export const compareVersion = compareSemver;
|
|
111
|
+
|
|
112
|
+
function splitVersion(v: string): { core: number[], pre: string[] | null } {
|
|
113
|
+
const hyphen = v.indexOf("-");
|
|
114
|
+
const core = hyphen === -1 ? v : v.slice(0, hyphen);
|
|
115
|
+
const prePart = hyphen === -1 ? null : v.slice(hyphen + 1);
|
|
116
|
+
return {
|
|
117
|
+
core: core.split(".").map(seg => Number.parseInt(seg, 10) || 0),
|
|
118
|
+
pre: prePart == null ? null : prePart.split("."),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function comparePrerelease(a: string[] | null, b: string[] | null): -1 | 0 | 1 {
|
|
123
|
+
if (a === null || b === null) {
|
|
124
|
+
return a === b ? 0 : a === null ? 1 : -1;
|
|
125
|
+
}
|
|
126
|
+
const len = Math.max(a.length, b.length);
|
|
127
|
+
for (let i = 0; i < len; i++) {
|
|
128
|
+
const ia = a[i];
|
|
129
|
+
const ib = b[i];
|
|
130
|
+
if (ia === undefined)
|
|
131
|
+
return -1;
|
|
132
|
+
if (ib === undefined)
|
|
133
|
+
return 1;
|
|
134
|
+
const na = /^\d+$/.test(ia);
|
|
135
|
+
const nb = /^\d+$/.test(ib);
|
|
136
|
+
if (na && nb) {
|
|
137
|
+
const diff = Number.parseInt(ia, 10) - Number.parseInt(ib, 10);
|
|
138
|
+
if (diff !== 0)
|
|
139
|
+
return diff < 0 ? -1 : 1;
|
|
140
|
+
}
|
|
141
|
+
else if (na !== nb) {
|
|
142
|
+
return na ? -1 : 1;
|
|
143
|
+
}
|
|
144
|
+
else if (ia !== ib) {
|
|
145
|
+
return ia < ib ? -1 : 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
76
149
|
}
|
package/src/market/publish.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface MarketPublishOptions {
|
|
|
36
36
|
privateKeyPem?: string
|
|
37
37
|
/** 落包后离线自检(verifyCanonical 复核包级签名) */
|
|
38
38
|
check?: boolean
|
|
39
|
+
/** 签发方 registry 身份(REG-CS3 v2 防跨仓库重放;透传 signCommand --registry-id) */
|
|
40
|
+
registryId?: string
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export interface MarketPublishResult {
|
|
@@ -73,7 +75,7 @@ export async function publishPackage(opts: MarketPublishOptions = {}): Promise<M
|
|
|
73
75
|
|
|
74
76
|
let signed: ReturnType<typeof signCommand>;
|
|
75
77
|
try {
|
|
76
|
-
signed = signCommand(tgzPath, { id: opts.id, key: opts.key, privateKeyPem: opts.privateKeyPem, check: opts.check });
|
|
78
|
+
signed = signCommand(tgzPath, { id: opts.id, key: opts.key, privateKeyPem: opts.privateKeyPem, check: opts.check, registryId: opts.registryId });
|
|
77
79
|
}
|
|
78
80
|
catch (error) {
|
|
79
81
|
rollbackPackage();
|
package/src/market/setup.ts
CHANGED
|
@@ -22,8 +22,8 @@ export const MARKET_CDN_URL_SETTING = "framework.market_cdn_url";
|
|
|
22
22
|
|
|
23
23
|
export interface SetupOptions {
|
|
24
24
|
projectRoot?: string
|
|
25
|
-
/** 站点地址(缺省取
|
|
26
|
-
|
|
25
|
+
/** 站点地址(缺省取 OJM_SITE_URL 或 http://127.0.0.1:9778) */
|
|
26
|
+
siteUrl?: string
|
|
27
27
|
user?: string
|
|
28
28
|
pass?: string
|
|
29
29
|
/** 新口令;不给则随机生成并只在终端打印一次(不落盘、不进日志) */
|
|
@@ -44,7 +44,7 @@ export interface SetupResult {
|
|
|
44
44
|
export async function setupMarketUrl(opts: SetupOptions = {}): Promise<SetupResult> {
|
|
45
45
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
46
46
|
const layout = resolveCdnLayout(projectRoot);
|
|
47
|
-
const site = resolveSite({
|
|
47
|
+
const site = resolveSite({ siteUrl: opts.siteUrl, user: opts.user, pass: opts.pass });
|
|
48
48
|
const fetchFn = opts.fetchFn ?? fetch;
|
|
49
49
|
|
|
50
50
|
const token = await obtainToken(fetchFn, site.base, site.user, site.pass, opts.newPass, projectRoot);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REG-CS5 装箱(D3):把「一批方案」装进分片,每片 ≤ `SHARD_MAX_VERSIONS` 个版本。
|
|
3
|
+
*
|
|
4
|
+
* 纯数据变换,不落 IO、不签名——编号与落盘归 `build-index.ts`。
|
|
5
|
+
*
|
|
6
|
+
* 三条策略(计划 §S2-1 子任务 2,D3 已定):
|
|
7
|
+
* 1. **整包装箱**:一个方案不跨片,除非它自己的版本数就超限——那种极端包才按连续上限切片,
|
|
8
|
+
* 且仍占连续片号(读方按片号倒序试取,最高片装最新版,`latest` 安装只拉 1 片)。
|
|
9
|
+
* 2. **确定性**:输入方案顺序即输出顺序(调用方已按名称字典序排好),同样目录内容永远装出
|
|
10
|
+
* 同样结果——「索引只取决于目录内容」这条性质靠它成立。
|
|
11
|
+
* 3. **累加到上限开新片**:按版本数往当前片累加,放不下就开新片(计划原文「按版本数累加到
|
|
12
|
+
* 上限开新片」,上限值经执行期实测校准,见下)。已有包因此天然不换片:只在它前面的内容变了才被挤动。
|
|
13
|
+
*
|
|
14
|
+
* 体积口径(需求 §7,2026-10-25 执行期实测校准):需求原文按「单条 Form B ≈535–684B」
|
|
15
|
+
* 估 2000 版本/片 ≈1.2MB;但本仓参考 manifest 形态(fixture.ts solutionManifest,945B YAML)
|
|
16
|
+
* 实测单条 Form B 条目 ≈1.2KB——2000/片 = 2.4MB,超计划 §S2-1「单片 ≤1.5MB」验收闸。
|
|
17
|
+
* 校准为 1000 版本/片:参考形态下 ≈1.2MB,正中需求目标;fat manifest 的最坏面也随砍半。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { BuiltSolution } from "./index-builder";
|
|
21
|
+
|
|
22
|
+
/** 单片最大版本数(REG-CS5:1000 版本 ≈1.2MB,计划验收 ≤1.5MB;实测依据见文件头) */
|
|
23
|
+
export const SHARD_MAX_VERSIONS = 1000;
|
|
24
|
+
|
|
25
|
+
/** 一个分片槽位(编号由 build-index 按内容匹配分配,这里只带内容) */
|
|
26
|
+
export interface ShardSlot {
|
|
27
|
+
/** 片内方案(顺序 = 装配顺序;版本在各方案内升序) */
|
|
28
|
+
solutions: BuiltSolution[]
|
|
29
|
+
/** 片内版本总数(≤SHARD_MAX_VERSIONS;超限包独占多片时每片恰为上限) */
|
|
30
|
+
versions: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 包名 → 所占片号(升序;读方倒序试取) */
|
|
34
|
+
export type ShardAssignment = Map<string, number[]>;
|
|
35
|
+
|
|
36
|
+
export interface PackedShards {
|
|
37
|
+
/** 槽位序列(顺序即装配顺序;编号未分配,见 ShardSlot) */
|
|
38
|
+
slots: ShardSlot[]
|
|
39
|
+
/** 包名 → 片号(升序) */
|
|
40
|
+
assignment: ShardAssignment
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 装箱。调用方保证 `solutions` 已按名称字典序(buildIndexData 的产出即如此)。
|
|
45
|
+
*
|
|
46
|
+
* 超限包(版本数 > `SHARD_MAX_VERSIONS`)切片时每片都是完整方案条目(带 name/latest),`latest` 恒取
|
|
47
|
+
* 包级真值——读方片号倒序,最高片必含最新版本,`latest` 只在最高片被消费;低片里的
|
|
48
|
+
* `latest` 是自洽兜底(指向本片最高版),不会被安装路径读到。
|
|
49
|
+
*/
|
|
50
|
+
export function packShards(solutions: BuiltSolution[]): PackedShards {
|
|
51
|
+
const slots: ShardSlot[] = [];
|
|
52
|
+
const assignment: ShardAssignment = new Map();
|
|
53
|
+
let current: ShardSlot = { solutions: [], versions: 0 };
|
|
54
|
+
|
|
55
|
+
for (const solution of solutions) {
|
|
56
|
+
if (solution.versions.length > SHARD_MAX_VERSIONS) {
|
|
57
|
+
// 超限包:先把当前片收尾,再按连续上限独占多片
|
|
58
|
+
if (current.solutions.length > 0) {
|
|
59
|
+
slots.push(current);
|
|
60
|
+
current = { solutions: [], versions: 0 };
|
|
61
|
+
}
|
|
62
|
+
const nos: number[] = [];
|
|
63
|
+
for (let at = 0; at < solution.versions.length; at += SHARD_MAX_VERSIONS) {
|
|
64
|
+
const chunk: BuiltSolution = {
|
|
65
|
+
name: solution.name,
|
|
66
|
+
latest: solution.latest,
|
|
67
|
+
versions: solution.versions.slice(at, at + SHARD_MAX_VERSIONS),
|
|
68
|
+
};
|
|
69
|
+
slots.push({ solutions: [chunk], versions: chunk.versions.length });
|
|
70
|
+
nos.push(slots.length); // 片号 = 槽位序号(1 起)
|
|
71
|
+
}
|
|
72
|
+
assignment.set(solution.name, nos);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (current.versions + solution.versions.length > SHARD_MAX_VERSIONS) {
|
|
76
|
+
slots.push(current);
|
|
77
|
+
current = { solutions: [], versions: 0 };
|
|
78
|
+
}
|
|
79
|
+
current.solutions.push(solution);
|
|
80
|
+
current.versions += solution.versions.length;
|
|
81
|
+
const nos = assignment.get(solution.name) ?? [];
|
|
82
|
+
nos.push(slots.length + 1); // 当前片编号 = 已收尾片数 + 1
|
|
83
|
+
assignment.set(solution.name, nos);
|
|
84
|
+
}
|
|
85
|
+
if (current.solutions.length > 0)
|
|
86
|
+
slots.push(current);
|
|
87
|
+
|
|
88
|
+
// 空索引也要有一个空片——catalog 的 shards 映射不为空,读方「拉 catalog 即知天下」
|
|
89
|
+
// (无片可拉时装任何包都走 404 分支,与空索引时代同语义)
|
|
90
|
+
if (slots.length === 0)
|
|
91
|
+
slots.push({ solutions: [], versions: 0 });
|
|
92
|
+
|
|
93
|
+
return { slots, assignment };
|
|
94
|
+
}
|
package/src/market/trial.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function trialInstall(opts: TrialOptions = {}): Promise<TrialResult
|
|
|
55
55
|
const newPass = "trial-pass-1";
|
|
56
56
|
const setup = await setupMarketUrl({
|
|
57
57
|
projectRoot: site.root,
|
|
58
|
-
|
|
58
|
+
siteUrl: site.baseUrl,
|
|
59
59
|
user: site.admin.user,
|
|
60
60
|
pass: site.admin.pass,
|
|
61
61
|
newPass,
|