@oj-module/cli 0.2.23 → 0.2.25
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 +7 -5
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +4 -3
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +13 -13
- package/src/__tests__/cli.test.ts +225 -0
- package/src/__tests__/layout.test.ts +8 -5
- package/src/__tests__/new-scaffold.test.ts +59 -0
- package/src/__tests__/usage.test.ts +45 -47
- package/src/build.ts +4 -4
- package/src/bundle.ts +9 -9
- package/src/cli.ts +265 -0
- package/src/contract/check.ts +4 -4
- package/src/dev.ts +17 -0
- package/src/index.ts +31 -211
- package/src/init.ts +2 -4
- package/src/keygen.ts +0 -19
- package/src/layout.ts +11 -11
- package/src/lifecycle/__tests__/authoring.test.ts +3 -14
- package/src/lifecycle/__tests__/bundle.test.ts +24 -24
- package/src/lifecycle/__tests__/install.test.ts +0 -23
- package/src/lifecycle/__tests__/new.test.ts +1 -1
- package/src/lifecycle/__tests__/smoke.test.ts +2 -2
- package/src/market/__tests__/bootstrap.test.ts +24 -0
- package/src/market/__tests__/setup.test.ts +8 -1
- package/src/market/bootstrap.ts +10 -8
- package/src/market/cdn-layout.ts +3 -3
- package/src/market/command.ts +0 -23
- package/src/market/setup.ts +14 -2
- package/src/market/trial.ts +3 -3
- package/src/market/walkthrough.ts +7 -6
- package/src/new.ts +21 -3
- package/src/preview-config.ts +2 -2
- package/src/preview.ts +5 -4
- package/src/publish.ts +0 -22
- package/src/sign.ts +0 -22
- package/src/usage.ts +482 -210
- package/templates/README.md +3 -3
- package/templates/cdn/README.md +6 -6
- package/templates/config.yaml +5 -4
- package/templates/init.include.yaml +1 -1
- package/templates/seed.mysql.sql +2 -1
- package/templates/seed.pg.sql +2 -1
- package/templates/src/_platform/api/market/api.ts +11 -8
- package/templates/src/_platform/api/seed.sql +2 -1
- package/templates/src/_platform/api/solutions/api.ts +2 -2
- package/templates/src/system/web/pages/users/roles.tsx +3 -1
- package/templates/tests/lifecycle/market-api.spec.ts +35 -5
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
- package/vendor/host-versions.json +3 -3
- package/src/args.ts +0 -172
- package/src/lifecycle/__tests__/args.test.ts +0 -20
package/templates/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
```bash
|
|
8
8
|
pnpm install
|
|
9
9
|
pnpm dev # 开发:前端 devServer + oj 后端(api/src 保存即热更)
|
|
10
|
-
pnpm build # 构建:oj build + 前端全站合并到 web
|
|
10
|
+
pnpm build # 构建:oj build + 前端全站合并到 dist/web
|
|
11
11
|
pnpm preview # 预览:oj migrate + server(release 产物)
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -35,9 +35,9 @@ pnpm exec ojm api # 重新生成 web/src/*/__client(dev
|
|
|
35
35
|
```
|
|
36
36
|
bin/ oj 二进制(init 解压,不入库)
|
|
37
37
|
api/src/ 后端模块源码(目录镜像路由:api/src/<模块>/<路径>/api.ts)
|
|
38
|
-
api/
|
|
38
|
+
dist/api/ oj build 产物
|
|
39
39
|
web/src/ 前端模块源码
|
|
40
|
-
web/
|
|
40
|
+
dist/web/ 构建产物(完整站点)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## 注意
|
package/templates/cdn/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# cdn/ —— 本地模拟市场源
|
|
2
2
|
|
|
3
|
-
本目录是**本地模拟 CDN**:`ojm market publish`
|
|
4
|
-
(`- prefix: /cdn` → `web:
|
|
5
|
-
|
|
3
|
+
本目录是**本地模拟 CDN**:`ojm market publish` 把分发包落在这里,站点通过工程根 `config.yaml` 里的挂载
|
|
4
|
+
(`- prefix: /cdn` → `web: ./cdn`)把它直出成分发源。市场按系统设置 `framework.market_cdn_url` 读取,
|
|
5
|
+
**seed 缺省值即相对路径 `/cdn`(本站点自供),新工程免 `ojm market setup`**;指向外部真 CDN 时才改设置。
|
|
6
6
|
|
|
7
7
|
> **目录缺失会让站点起不来**:`oj serve` 启动即校验挂载目录,缺失就报错退出。
|
|
8
8
|
> 所以本 README 进模板、进发布包、进新工程——它的存在就是目录存在的理由,**不要删**。
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
## 生产怎么切出去
|
|
25
25
|
|
|
26
|
-
1.
|
|
26
|
+
1. 删掉工程根 `config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
|
|
27
27
|
|
|
28
28
|
```yaml
|
|
29
29
|
- prefix: /cdn
|
|
30
|
-
web:
|
|
30
|
+
web: ./cdn
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
2. 把真 CDN(对象存储 / nginx
|
|
33
|
+
2. 把真 CDN(对象存储 / nginx)的**绝对地址**写进系统设置 `framework.market_cdn_url`;
|
|
34
34
|
命令行等价写法是 `ojm market setup --url <地址>`。
|
|
35
35
|
3. 索引(`bundles.yaml` / `manifests.yaml`)由 CDN 侧维护:`ojm market build-index` 在本地产出后自行上传。
|
|
36
36
|
|
package/templates/config.yaml
CHANGED
|
@@ -30,19 +30,20 @@
|
|
|
30
30
|
# **这里没有 api 挂载是有意的**:本工程是 `src/<name>/{api,web}` 布局,模块分散在
|
|
31
31
|
# `src/<name>/api` 下,而 mounts 的 api 项按 `<目录>/<模块>/manifest.yaml` 单层解析、
|
|
32
32
|
# 不会递归(实测报 `module 'x' missing manifest.yaml`)。源码形态由 `ojm dev` 用
|
|
33
|
-
# `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path api
|
|
33
|
+
# `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path dist/api` 供给;
|
|
34
34
|
# 两者与 mounts 的 api 项同时出现还会报「module declared in multiple places」。
|
|
35
35
|
# 手写部署(不经 ojm)时才需要自己加回一条 `- prefix: /api` + `api: dist`。
|
|
36
36
|
mounts:
|
|
37
37
|
# 本地模拟 CDN:直出工程根 cdn/(内容见 cdn/README.md)。
|
|
38
|
-
#
|
|
38
|
+
# seed 的 framework.market_cdn_url 缺省即 /cdn(本站点自供),新工程免 market setup。
|
|
39
|
+
# 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN(绝对地址)。
|
|
39
40
|
# 不得给此挂载设 spa——缺文件必须返回 404,回落 index.html 会让市场解析崩。
|
|
40
41
|
- prefix: /cdn
|
|
41
42
|
web: ./cdn
|
|
42
43
|
# 生产若由 oj 直出前端(前后端同进程,即 ojm preview 的生产形态),
|
|
43
|
-
# 取消下一行注释并先 build 出 web
|
|
44
|
+
# 取消下一行注释并先 build 出 dist/web(preview 会自动注入等价挂载,无需手改):
|
|
44
45
|
# - prefix: "/"
|
|
45
|
-
# web: "./web
|
|
46
|
+
# web: "./dist/web" # 相对 config.yaml → 工程根/dist/web
|
|
46
47
|
# spa: true
|
|
47
48
|
# 嵌套 handler 组(oj v0.1.64)无需配置开关,组织方式见 api.ts 顶部注释。
|
|
48
49
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# 2. 模板自运行专属文件——templates 本身也是一个可 `pnpm dev` 的工程,
|
|
9
9
|
# 它的 package.json / node_modules / bin / lock 绝不能泄漏进用户工程;
|
|
10
10
|
# package.json 由 init 现场生成(钉死真实版本,不带 workspace:/catalog:)。
|
|
11
|
-
# 3. 构建与运行期产物——api/
|
|
11
|
+
# 3. 构建与运行期产物——dist/api、dist/web、logs、签发的私钥与 sqlite;
|
|
12
12
|
# 模板跑过 build/dev 后会留在本地,既不进用户工程,更不该让所有工程
|
|
13
13
|
# 共享同一把私钥。
|
|
14
14
|
# 4. 模板自用元数据——本清单自身。
|
package/templates/seed.mysql.sql
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
-- 系统设置默认值(PRD §4.3;注释内禁英文分号)
|
|
2
2
|
INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
|
|
3
|
-
|
|
3
|
+
-- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
|
|
4
|
+
INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
|
|
4
5
|
INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
|
|
5
6
|
INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
|
package/templates/seed.pg.sql
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
-- 系统设置默认值(PRD §4.3;注释内禁英文分号)
|
|
2
2
|
INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0) ON CONFLICT DO NOTHING;
|
|
3
|
-
|
|
3
|
+
-- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
|
|
4
|
+
INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0) ON CONFLICT DO NOTHING;
|
|
4
5
|
INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0) ON CONFLICT DO NOTHING;
|
|
5
6
|
INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0) ON CONFLICT DO NOTHING;
|
|
@@ -87,7 +87,7 @@ export interface MarketEnv {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* 宿主 versions.json 布局推导(B-11):与 manifests.yaml 同层(release 根 = `sharedRoot`),
|
|
90
|
-
* 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`api
|
|
90
|
+
* 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`dist/api` vs `dist/web`)。
|
|
91
91
|
*/
|
|
92
92
|
export function versionsJsonPathOf(releaseRoot: string): string {
|
|
93
93
|
const trimmed = releaseRoot.replace(/\/+$/, "");
|
|
@@ -132,21 +132,22 @@ async function hostVersionsOf(env: MarketEnv): Promise<Record<string, string> |
|
|
|
132
132
|
/**
|
|
133
133
|
* oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖)。
|
|
134
134
|
*
|
|
135
|
-
* 两处根在 `src/<name>/{api,web}`
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* 两处根在 `src/<name>/{api,web}` 布局下分流(2026-10-22 起收进工程根 dist/),
|
|
136
|
+
* 且都相对 fs jail 根(= config.yaml 所在目录):
|
|
137
|
+
* release 根 `dist/api` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
|
|
138
|
+
* 站点根 `dist/web` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
|
|
138
139
|
* 暂存 / 任务 / 状态 / 信任库 / db 仍在 jail 根下:它们是安装器私有面,不属任何产物根。
|
|
139
140
|
*/
|
|
140
141
|
export function marketEnv(): MarketEnv {
|
|
141
142
|
return {
|
|
142
|
-
sharedRoot: "api
|
|
143
|
+
sharedRoot: "dist/api",
|
|
143
144
|
staging: "modules/.staging",
|
|
144
145
|
taskDir: "modules/.install-tasks",
|
|
145
146
|
statePath: "modules/.solutions/state.yaml",
|
|
146
147
|
trustPath: ".solutions/trusted-keys.yaml",
|
|
147
148
|
dbPath: "db.sqlite",
|
|
148
|
-
siteModulesDir: "web/
|
|
149
|
-
modulesJsonPath: "web/
|
|
149
|
+
siteModulesDir: "dist/web/modules",
|
|
150
|
+
modulesJsonPath: "dist/web/modules.json",
|
|
150
151
|
apiPrefix: "/api",
|
|
151
152
|
ledgerHeadOf: journalHeadOf,
|
|
152
153
|
};
|
|
@@ -162,7 +163,9 @@ function fail(status: number, msg: string): never {
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
function fetchOf(env: MarketEnv): FetchInitFace {
|
|
165
|
-
|
|
166
|
+
// 相对基址(feature:/cdn = 本站点自供)按请求源补全同源绝对 URL;绝对 http(s) 直通。
|
|
167
|
+
// 无请求上下文(无 Host)不猜原点、原样透传;runInstall 侧的 withSiteOrigin 包裹幂等。
|
|
168
|
+
return withSiteOrigin(env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace);
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
function execOf(env: MarketEnv): ExecFace {
|
|
@@ -23,8 +23,9 @@ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM
|
|
|
23
23
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
|
|
24
24
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
|
|
25
25
|
-- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;framework.market_cdn_url 安全组仅 superadmin 可改)
|
|
26
|
+
-- framework.market_cdn_url 缺省 /cdn = 本站点自供(config.yaml 的 /cdn 挂载直出工程根 cdn/),脚手架免 setup;外部真 CDN 才改指绝对地址
|
|
26
27
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
|
|
27
|
-
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
|
|
28
|
+
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
|
|
28
29
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
|
|
29
30
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
|
|
30
31
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
|
|
@@ -37,9 +37,9 @@ export type { SolutionRouteRow } from "../_lifecycle/verify-checks";
|
|
|
37
37
|
*/
|
|
38
38
|
export function solutionsEnv(): SolutionsEnv {
|
|
39
39
|
return {
|
|
40
|
-
sharedRoot: "api
|
|
40
|
+
sharedRoot: "dist/api",
|
|
41
41
|
statePath: "modules/.solutions/state.yaml",
|
|
42
|
-
modulesJsonPath: "web/
|
|
42
|
+
modulesJsonPath: "dist/web/modules.json",
|
|
43
43
|
apiPrefix: "/api",
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
|
|
7
7
|
import { useNavigate, useParams } from "react-router";
|
|
8
8
|
|
|
9
9
|
import { systemRolesGet, systemUsersGet, systemUsersRolesPut } from "../../__client/api";
|
|
10
|
-
import { isForbidden } from "../errors";
|
|
10
|
+
import { explainError, isForbidden } from "../errors";
|
|
11
11
|
import { ForbiddenPanel } from "../forbidden";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -36,6 +36,8 @@ export default function UserRolesPage() {
|
|
|
36
36
|
queryClient.invalidateQueries({ queryKey: ["system", "users"] });
|
|
37
37
|
navigate("/system/users");
|
|
38
38
|
},
|
|
39
|
+
// 403 全局 toast 已被请求层抑制,页面须自行反馈(admin 非 superadmin 手输 URL 的场景)
|
|
40
|
+
onError: e => message.error(explainError(e, t)),
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
const user = usersQuery.data?.find(u => String(u.id) === uid);
|
|
@@ -331,6 +331,36 @@ describe("POST install(cdn:Form B 索引解析属市场侧,安装器不拼
|
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
describe("相对基址(feature:/cdn = 本站点自供,按请求源补全同源绝对 URL)", () => {
|
|
335
|
+
const REL = "/cdn";
|
|
336
|
+
const HOST = "127.0.0.1:9778";
|
|
337
|
+
const relEnv = () => makeEnv({ cdnBase: async () => REL });
|
|
338
|
+
|
|
339
|
+
it("index:索引与 .sig/manifests 拉取按请求 Host 补全为同源绝对 URL", async () => {
|
|
340
|
+
const api = createMarketApi(relEnv());
|
|
341
|
+
g.http = { user: { id: 1 }, headers: { host: HOST } };
|
|
342
|
+
await api.index.get();
|
|
343
|
+
expect(last?.status).toBe(200);
|
|
344
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/bundles.yaml`);
|
|
345
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/manifests.yaml`);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("install:archive 分发包同样补全同源绝对 URL", async () => {
|
|
349
|
+
const api = createMarketApi(relEnv());
|
|
350
|
+
g.http = { user: { id: 1 }, body: { name: "demo-solution", version: "0.1.0", source: "cdn" }, headers: { host: HOST } };
|
|
351
|
+
await api.install.post();
|
|
352
|
+
expect(last?.status).toBe(200);
|
|
353
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/demo-solution-0.1.0.tgz`);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("无请求上下文(无 Host):不猜原点,原样透传相对 URL", async () => {
|
|
357
|
+
const api = createMarketApi(relEnv());
|
|
358
|
+
g.http = { user: { id: 1 } };
|
|
359
|
+
await api.index.get();
|
|
360
|
+
expect(fetchCalls).toContain("/cdn/bundles.yaml");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
334
364
|
describe("POST install/cancel(P3-12 端点接线,U5/P3-8 令牌的消费面)", () => {
|
|
335
365
|
it("无进行中安装 → 404(取消只对 running 态有效,任务终态后信号已注销)", async () => {
|
|
336
366
|
const api = createMarketApi(makeEnv());
|
|
@@ -858,12 +888,12 @@ describe("hostVersions 缺省接线(B-11:C2-09 比对真源 = <dist>/version
|
|
|
858
888
|
});
|
|
859
889
|
|
|
860
890
|
describe("P1-16 接线:marketEnv 生产口径 / exec opts 转发 / operator 通道 / 账本真源 / index 即对账入口", () => {
|
|
861
|
-
it("marketEnv() 缺省 = 布局
|
|
891
|
+
it("marketEnv() 缺省 = 布局 v3 生产口径:版本目录与 versions.json 在 release 根 dist/api,站点 modules 在 dist/web;暂存/任务/sidecar 留 jail 根", () => {
|
|
862
892
|
const env = marketEnv();
|
|
863
|
-
expect(env.sharedRoot).toBe("api
|
|
864
|
-
expect(env.siteModulesDir).toBe("web/
|
|
865
|
-
expect(env.modulesJsonPath).toBe("web/
|
|
866
|
-
expect(versionsJsonPathOf(env.sharedRoot)).toBe("api/
|
|
893
|
+
expect(env.sharedRoot).toBe("dist/api");
|
|
894
|
+
expect(env.siteModulesDir).toBe("dist/web/modules");
|
|
895
|
+
expect(env.modulesJsonPath).toBe("dist/web/modules.json");
|
|
896
|
+
expect(versionsJsonPathOf(env.sharedRoot)).toBe("dist/api/versions.json");
|
|
867
897
|
expect(env.staging).toBe("modules/.staging");
|
|
868
898
|
expect(env.taskDir).toBe("modules/.install-tasks");
|
|
869
899
|
expect(env.statePath).toBe("modules/.solutions/state.yaml");
|
|
@@ -278,7 +278,7 @@ function setHttp(over: Record<string, unknown>) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
function makeEnv() {
|
|
281
|
-
// modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `web/
|
|
281
|
+
// modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `dist/web/modules.json`
|
|
282
282
|
// 是相对 cwd 的,会把模板本机构建产物卷进用例,全绿用例随「模板 build 过没有」摇摆。
|
|
283
283
|
// P4-2 两条 integrity 用例显式覆盖它。
|
|
284
284
|
return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json") };
|
|
@@ -128,7 +128,7 @@ function setHttp(over: Record<string, unknown> = {}) {
|
|
|
128
128
|
|
|
129
129
|
function makeEnv() {
|
|
130
130
|
// modulesJsonPath 同 verify-checks.spec:钉进临时目录(缺文件 → 跳过 web 项),
|
|
131
|
-
// 不吃模板本机 `web/
|
|
131
|
+
// 不吃模板本机 `dist/web/modules.json`。
|
|
132
132
|
return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json"), verifyReportsDir: reportsDir };
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.25",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.25",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.25",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|
package/src/args.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 子命令参数解析(纯函数,可测)。
|
|
3
|
-
*
|
|
4
|
-
* 约定:位置参数取第一个不以 `--` 开头的参数;`--xxx` 一律是开关/选项。
|
|
5
|
-
* 缺陷背景:`ojm init --yes` 曾把 argv[3](即 `--yes`)当目标目录。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export function parseInitArgs(argv: string[]): { dest: string, yes: boolean } {
|
|
9
|
-
const yes = argv.includes("--yes");
|
|
10
|
-
const dest = argv.find(a => !a.startsWith("--")) ?? "";
|
|
11
|
-
return { dest, yes };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* `ojm api [dir] [--check] [--docs] [--exempt <path>]`:位置参数为项目目录
|
|
16
|
-
* (缺省 cwd),与 parseInitArgs 同一约定(docs/prd/202609032019-ojm-api-cwd.md)。
|
|
17
|
-
* 注意:`--exempt` 的「值」不得以 `--` 开头,否则会被误判为 dir——故逐项扫描,
|
|
18
|
-
* 消费掉 `--exempt` 紧随的值,避免被 dir 解析吞噬(P6 同款坑)。
|
|
19
|
-
*/
|
|
20
|
-
export function parseApiArgs(argv: string[]): { dir: string, check: boolean, docs: boolean, exempt?: string } {
|
|
21
|
-
const flags = new Set<string>();
|
|
22
|
-
const positionals: string[] = [];
|
|
23
|
-
let exempt: string | undefined;
|
|
24
|
-
for (let i = 0; i < argv.length; i++) {
|
|
25
|
-
const a = argv[i];
|
|
26
|
-
if (a === "--exempt") {
|
|
27
|
-
exempt = argv[i + 1];
|
|
28
|
-
i++; // 跳过紧随的值,不被当 dir
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (a.startsWith("--")) {
|
|
32
|
-
flags.add(a);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
positionals.push(a);
|
|
36
|
-
}
|
|
37
|
-
const dir = positionals[0] ?? "";
|
|
38
|
-
return { dir, check: flags.has("--check"), docs: flags.has("--docs"), exempt };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* `ojm build [--bundle] [--out <dir>]`。
|
|
43
|
-
* `--bundle`:方案包构建(契约校验 → 确定性 tgz + sha256)。bundle 级契约位置:优先工程根
|
|
44
|
-
* `solution.yaml`、或 `src/<模块>/solution.yaml`(模块级方案,契约与所打包模块同位);旧方案回退
|
|
45
|
-
* `manifest.yaml`(同两处位置)。与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工
|
|
46
|
-
* (devkit 手册「打包与分发」)。
|
|
47
|
-
* `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
|
|
48
|
-
*/
|
|
49
|
-
export function parseBuildArgs(argv: string[]): { bundle: boolean, bundleName?: string, out?: string } {
|
|
50
|
-
const flags = new Set<string>();
|
|
51
|
-
let out: string | undefined;
|
|
52
|
-
let bundleName: string | undefined;
|
|
53
|
-
for (let i = 0; i < argv.length; i++) {
|
|
54
|
-
const a = argv[i];
|
|
55
|
-
if (a === "--out") {
|
|
56
|
-
out = argv[i + 1];
|
|
57
|
-
i++; // 消费紧随的值,不被当 flag
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
// --bundle <name> / --bundle=<name>:只打这一份方案(无值 = 全部用户方案)
|
|
61
|
-
if (a === "--bundle") {
|
|
62
|
-
flags.add(a);
|
|
63
|
-
const next = argv[i + 1];
|
|
64
|
-
if (next !== undefined && !next.startsWith("--")) {
|
|
65
|
-
bundleName = next;
|
|
66
|
-
i++;
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (a.startsWith("--bundle=")) {
|
|
71
|
-
flags.add("--bundle");
|
|
72
|
-
bundleName = a.slice("--bundle=".length);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (a.startsWith("--")) {
|
|
76
|
-
flags.add(a);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return { bundle: flags.has("--bundle"), bundleName, out };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* `ojm new <name> [--layout container|none] [--api]`:新方案骨架生成参数。
|
|
84
|
-
* name 为首个非 flag 位置参数(必填,由调用方判空——非交互/CI 友好)。
|
|
85
|
-
*/
|
|
86
|
-
export function parseNewArgs(argv: string[]): { name?: string, layout?: "container" | "none", api?: boolean } {
|
|
87
|
-
let name: string | undefined;
|
|
88
|
-
let layout: "container" | "none" | undefined;
|
|
89
|
-
let api = false;
|
|
90
|
-
for (let i = 0; i < argv.length; i++) {
|
|
91
|
-
const a = argv[i];
|
|
92
|
-
if (a === "--layout") {
|
|
93
|
-
const v = argv[i + 1];
|
|
94
|
-
if (v !== "container" && v !== "none")
|
|
95
|
-
throw new Error(`[ojm] --layout 只接受 container | none(实际:${v})`);
|
|
96
|
-
layout = v;
|
|
97
|
-
i++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (a === "--api") {
|
|
101
|
-
api = true;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (a !== undefined && !a.startsWith("--")) {
|
|
105
|
-
if (name !== undefined)
|
|
106
|
-
throw new Error(`[ojm] ojm new 只接受一个方案名(多余参数:${a})`);
|
|
107
|
-
name = a;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { name, layout, api };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* `ojm vendor [tag] [--force]`:下载/更新 oj vendor(npm 包 @oj-bin/oj)。
|
|
116
|
-
* tag 缺省追 npm latest;形如 `v0.1.13` 或 `0.1.13`(自动补 v 前缀,装包时去掉)。
|
|
117
|
-
* 非法 tag 形状即报错(防误把目录当版本)。
|
|
118
|
-
*/
|
|
119
|
-
export function parseVendorArgs(argv: string[]): { force: boolean, tag?: string } {
|
|
120
|
-
const tag = argv.find(a => !a.startsWith("--") && a !== "--");
|
|
121
|
-
if (tag !== undefined && !/^v?\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(tag)) {
|
|
122
|
-
throw new Error(`[ojm] 非法版本号:${tag}(形如 v0.1.11 或 0.1.11)`);
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
force: argv.includes("--force"),
|
|
126
|
-
tag: tag === undefined ? undefined : (tag.startsWith("v") ? tag : `v${tag}`),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface LifecycleArgs {
|
|
131
|
-
positionals: string[]
|
|
132
|
-
url?: string
|
|
133
|
-
user?: string
|
|
134
|
-
pass?: string
|
|
135
|
-
locale?: string
|
|
136
|
-
name?: string
|
|
137
|
-
version?: string
|
|
138
|
-
task?: string
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* `ojm install|enable|disable` 共用解析:位置参数(目标/方案名)+ 值式 flag。
|
|
143
|
-
* 站点寻址三件套 --url/--user/--pass(环境变量 OJM_URL/OJM_USER/OJM_PASS 兜底,见 src/site.ts);
|
|
144
|
-
* install 专属 --name/--version(文件名身份覆盖)与 --locale(终态文案语言);
|
|
145
|
-
* trust 专属 --task(TOFU 确认目标任务 id,P2-3)。
|
|
146
|
-
* 值式 flag 逐项消费紧随的值,不被当位置参数(P6 同款坑,parseApiArgs 同纪律)。
|
|
147
|
-
*/
|
|
148
|
-
export function parseLifecycleArgs(argv: string[]): LifecycleArgs {
|
|
149
|
-
const valueFlags = new Set(["--url", "--user", "--pass", "--locale", "--name", "--version", "--task"]);
|
|
150
|
-
const values: Record<string, string> = {};
|
|
151
|
-
const positionals: string[] = [];
|
|
152
|
-
for (let i = 0; i < argv.length; i++) {
|
|
153
|
-
const a = argv[i]!;
|
|
154
|
-
if (valueFlags.has(a)) {
|
|
155
|
-
values[a] = argv[i + 1] ?? "";
|
|
156
|
-
i++; // 消费紧随的值
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (!a.startsWith("--"))
|
|
160
|
-
positionals.push(a);
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
positionals,
|
|
164
|
-
url: values["--url"],
|
|
165
|
-
user: values["--user"],
|
|
166
|
-
pass: values["--pass"],
|
|
167
|
-
locale: values["--locale"],
|
|
168
|
-
name: values["--name"],
|
|
169
|
-
version: values["--version"],
|
|
170
|
-
task: values["--task"],
|
|
171
|
-
};
|
|
172
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// BDD:ojm new 参数解析(统一审查 F8——多余位置参数须显式拒绝,不能静默丢弃)。
|
|
2
|
-
// 静默丢弃的恶果:ojm new crm erp(手滑/粘贴)只建 crm,用户以为两份都建了。
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { parseNewArgs } from "../../args";
|
|
5
|
-
|
|
6
|
-
describe("parseNewArgs", () => {
|
|
7
|
-
it("name + 旗标组合", () => {
|
|
8
|
-
expect(parseNewArgs(["crm"])).toEqual({ name: "crm", layout: undefined, api: false });
|
|
9
|
-
expect(parseNewArgs(["crm", "--layout", "none", "--api"])).toEqual({ name: "crm", layout: "none", api: true });
|
|
10
|
-
expect(parseNewArgs([])).toEqual({ name: undefined, layout: undefined, api: false });
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("多余位置参数 → 报错而非静默丢弃", () => {
|
|
14
|
-
expect(() => parseNewArgs(["crm", "erp"])).toThrow(/只接受一个方案名/);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("非法 --layout 值 → 报错", () => {
|
|
18
|
-
expect(() => parseNewArgs(["crm", "--layout", "sidebar"])).toThrow(/container \| none/);
|
|
19
|
-
});
|
|
20
|
-
});
|