@lark-apaas/miaoda-cli 0.1.33 → 0.1.35
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/dist/cli/commands/deploy/modern.js +6 -5
- package/dist/services/deploy/modern/patch/actions.js +6 -4
- package/dist/services/deploy/modern/patch/html-title.js +75 -0
- package/dist/services/deploy/modern/patch/index.js +1 -1
- package/dist/services/deploy/modern/patch/routes.js +13 -3
- package/package.json +1 -1
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +28 -7
|
@@ -72,7 +72,7 @@ JSON 输出(stdout)
|
|
|
72
72
|
}));
|
|
73
73
|
const patchCmd = deployCmd
|
|
74
74
|
.command('patch')
|
|
75
|
-
.description('design-html 增量发布:按文件 create/update/delete,.html
|
|
75
|
+
.description('design-html 增量发布:按文件 create/update/delete,.html 变动时同步 routes.json')
|
|
76
76
|
.option('--create <relpath>', '新增文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
77
77
|
.option('--update <relpath>', '覆盖已有文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
78
78
|
.option('--delete <relpath>', '删除文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
@@ -80,10 +80,11 @@ JSON 输出(stdout)
|
|
|
80
80
|
.addHelpText('after', `
|
|
81
81
|
--dir 为项目目录(与 deploy 同名参数,默认当前目录)。
|
|
82
82
|
仅支持 design-html(design_local_deploy)。路径为工程根相对路径(= 服务端 latest 下 key),
|
|
83
|
-
禁绝对路径与 ".."。--create/--update
|
|
84
|
-
时自动重算并随包更新 routes.json
|
|
85
|
-
routes.json 每项为 {path, file}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
-
|
|
83
|
+
禁绝对路径与 ".."。--create/--update 的本地文件必须存在。改动里含任意 .html(增 / 改 / 删)
|
|
84
|
+
时自动重算并随包更新 routes.json;纯非 .html 改动不动 routes.json。
|
|
85
|
+
routes.json 每项为 {path, file, name?}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
+
file 为对应源文件(如 / → index.html),name 为该 HTML 的 <title> 文本(方案名,可缺省,
|
|
87
|
+
缺省时由消费侧 fallback 到 path)。
|
|
87
88
|
|
|
88
89
|
JSON 输出
|
|
89
90
|
{"data": {"upsertCount": <n>, "deleteCount": <n>, "actionsSent": <n>, "routesRegenerated": <bool>}}
|
|
@@ -36,8 +36,10 @@ function isHtml(rel) {
|
|
|
36
36
|
/**
|
|
37
37
|
* 把 changeset 组成 TosFileAction[]:
|
|
38
38
|
* - create/update 读本地文件、自动编码;delete 仅 FilePath。
|
|
39
|
-
* - .html 被 create / delete
|
|
40
|
-
*
|
|
39
|
+
* - 任一 .html 被 create / update / delete 时,追加重算 routes.json 的 UPDATE action。
|
|
40
|
+
* update 也算:routes.json 的 name 取自 HTML 的 `<title>`,改方案名就是一次纯 update,
|
|
41
|
+
* 只按 create/delete 判定会让改名永远进不了 routes.json。
|
|
42
|
+
* - 纯非 .html 改动(js / css / 图片)不动 routes.json。
|
|
41
43
|
*/
|
|
42
44
|
function buildTosActions(projectDir, cs) {
|
|
43
45
|
const actions = [];
|
|
@@ -48,8 +50,8 @@ function buildTosActions(projectDir, cs) {
|
|
|
48
50
|
for (const rel of cs.deletes) {
|
|
49
51
|
actions.push({ actionType: index_1.TosActionType.DELETE, filePath: normalizeRel(rel) });
|
|
50
52
|
}
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
+
const routesMayChange = [...cs.creates, ...cs.updates, ...cs.deletes].some(isHtml);
|
|
54
|
+
if (routesMayChange) {
|
|
53
55
|
actions.push({
|
|
54
56
|
actionType: index_1.TosActionType.UPDATE,
|
|
55
57
|
filePath: 'routes.json',
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractTitle = extractTitle;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
/**
|
|
9
|
+
* 读 `<title>` 时的扫描窗口上限(字节)。title 规范上在 `<head>` 里、正常紧贴文档开头,
|
|
10
|
+
* 64KB 足以覆盖 head 里塞了内联 style / script 的情况,同时给大 HTML 兜住 IO 上限
|
|
11
|
+
* (generateRoutes 会遍历工程内所有 .html,不能整文件读)。
|
|
12
|
+
*/
|
|
13
|
+
const HEAD_SCAN_BYTES = 64 * 1024;
|
|
14
|
+
const TITLE_RE = /<title[^>]*>([^<]*)<\/title>/i;
|
|
15
|
+
const HEAD_CLOSE_RE = /<\/head\s*>/i;
|
|
16
|
+
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
17
|
+
const ENTITY_RE = /&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi;
|
|
18
|
+
/** HTML 里可能出现在 title 文本中的命名实体。未列出的原样保留,不做全量实体表。 */
|
|
19
|
+
const NAMED_ENTITIES = {
|
|
20
|
+
amp: '&',
|
|
21
|
+
lt: '<',
|
|
22
|
+
gt: '>',
|
|
23
|
+
quot: '"',
|
|
24
|
+
apos: "'",
|
|
25
|
+
nbsp: ' ',
|
|
26
|
+
};
|
|
27
|
+
/** 把 title 文本里的实体还原成明文——name 是给前端直接当文本渲染的,不该带 `&`。 */
|
|
28
|
+
function decodeEntities(s) {
|
|
29
|
+
return s.replace(ENTITY_RE, (matched, body) => {
|
|
30
|
+
if (!body.startsWith('#')) {
|
|
31
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? matched;
|
|
32
|
+
}
|
|
33
|
+
const code = /^#x/i.test(body) ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
34
|
+
if (!Number.isInteger(code) || code <= 0 || code > 0x10ffff)
|
|
35
|
+
return matched;
|
|
36
|
+
return String.fromCodePoint(code);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** 只读文件头部 HEAD_SCAN_BYTES 字节。尾部可能截断多字节字符,对 title 提取无影响。 */
|
|
40
|
+
function readHead(absPath) {
|
|
41
|
+
const fd = node_fs_1.default.openSync(absPath, 'r');
|
|
42
|
+
try {
|
|
43
|
+
const buf = Buffer.alloc(HEAD_SCAN_BYTES);
|
|
44
|
+
const bytes = node_fs_1.default.readSync(fd, buf, 0, HEAD_SCAN_BYTES, 0);
|
|
45
|
+
return buf.subarray(0, bytes).toString('utf-8');
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
node_fs_1.default.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 取 HTML 的 `<title>` 文本作为该页面的方案名。
|
|
53
|
+
*
|
|
54
|
+
* - 扫描范围截到第一个 `</head>`:body 里的 `<title>` 不算——内联 SVG 图标的 a11y
|
|
55
|
+
* `<title>` 很常见,抓到就会把图标描述当成方案名。找不到 `</head>` 时退回整个窗口。
|
|
56
|
+
* - 先剔 HTML 注释,避免抓到被注释掉的 title。
|
|
57
|
+
* - 实体还原 + 空白折叠 + trim;空 title / 无 title / 读失败一律返回 undefined,
|
|
58
|
+
* 由消费侧 fallback 到 path(旧应用没有 title 时保持显示页面地址)。
|
|
59
|
+
*/
|
|
60
|
+
function extractTitle(absPath) {
|
|
61
|
+
let head;
|
|
62
|
+
try {
|
|
63
|
+
head = readHead(absPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const headClose = HEAD_CLOSE_RE.exec(head)?.index ?? -1;
|
|
69
|
+
const scope = (headClose >= 0 ? head.slice(0, headClose) : head).replace(COMMENT_RE, '');
|
|
70
|
+
const raw = TITLE_RE.exec(scope)?.[1];
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const text = decodeEntities(raw).replace(/\s+/g, ' ').trim();
|
|
74
|
+
return text === '' ? undefined : text;
|
|
75
|
+
}
|
|
@@ -11,7 +11,7 @@ const actions_1 = require("./actions");
|
|
|
11
11
|
var actions_2 = require("./actions");
|
|
12
12
|
Object.defineProperty(exports, "buildTosActions", { enumerable: true, get: function () { return actions_2.buildTosActions; } });
|
|
13
13
|
/**
|
|
14
|
-
* design-html 增量发布:读本地文件组 TosFileAction
|
|
14
|
+
* design-html 增量发布:读本地文件组 TosFileAction(改动含 .html 时带重算的
|
|
15
15
|
* routes.json),调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
|
|
16
16
|
*/
|
|
17
17
|
async function patchDesignDeploy(opts) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.generateRoutes = generateRoutes;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const html_title_1 = require("./html-title");
|
|
4
9
|
const source_scan_1 = require("./source-scan");
|
|
5
10
|
/**
|
|
6
11
|
* .html → route path 推导(相对路径,不带 base 前缀)。rel 为 posix 相对路径。
|
|
@@ -16,8 +21,9 @@ function toRoute(rel) {
|
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* 扫描 projectDir 下所有 .html(套 source-scan 的 EXCLUDES),生成 routes.json 文本(含尾换行)。
|
|
19
|
-
* 每项为 { path, file }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
20
|
-
* file 为该路由对应的原始 .html
|
|
24
|
+
* 每项为 { path, file, name? }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
25
|
+
* file 为该路由对应的原始 .html 源文件,name 为该文件 `<title>` 解析出的方案名(可缺省)。
|
|
26
|
+
* 按 path 排序、同 path 取字典序靠前的 file 去重。
|
|
21
27
|
* 零 html 时输出空数组——不伪造解析不到文件的根路由。
|
|
22
28
|
*/
|
|
23
29
|
function generateRoutes(projectDir) {
|
|
@@ -27,7 +33,11 @@ function generateRoutes(projectDir) {
|
|
|
27
33
|
if (!byPath.has(entry.path))
|
|
28
34
|
byPath.set(entry.path, entry);
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
// title 只读去重后胜出的 file:同 path 多源(foo.html / foo/index.html)时不做无用 IO。
|
|
37
|
+
const routes = [...byPath.values()].map((entry) => {
|
|
38
|
+
const name = (0, html_title_1.extractTitle)(node_path_1.default.join(projectDir, entry.file));
|
|
39
|
+
return name === undefined ? entry : { ...entry, name };
|
|
40
|
+
});
|
|
31
41
|
return JSON.stringify(routes, null, 2) + '\n';
|
|
32
42
|
}
|
|
33
43
|
function byPathThenFile(a, b) {
|
package/package.json
CHANGED
|
@@ -63,13 +63,32 @@ function isStylelintTarget(filePath) {
|
|
|
63
63
|
return filePath.endsWith('.css');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const STYLELINT_CONFIG_FILES = [
|
|
67
|
+
'.stylelintrc',
|
|
68
|
+
'.stylelintrc.js',
|
|
69
|
+
'.stylelintrc.cjs',
|
|
70
|
+
'.stylelintrc.json',
|
|
71
|
+
'stylelint.config.js',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 老模板(fullstack-nestjs-template 1.x 时代)的应用没有 stylelint 配置,跑 stylelint 会以
|
|
76
|
+
* "ConfigurationError: No configuration provided" 挂掉、把 pre-commit 卡死。没配置就跳过。
|
|
77
|
+
*/
|
|
78
|
+
function hasStylelintConfig() {
|
|
79
|
+
return STYLELINT_CONFIG_FILES.some(file => fs.existsSync(path.join(cwd, file)));
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
async function runDefaultLint() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'npm run
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
const commands = ['npm run eslint', 'npm run type:check'];
|
|
84
|
+
|
|
85
|
+
if (hasStylelintConfig()) {
|
|
86
|
+
commands.push('npm run stylelint');
|
|
87
|
+
} else {
|
|
88
|
+
console.warn('[lint] Skip stylelint: no stylelint config found');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const code = await runCommand(getBinName('npx'), ['concurrently', ...commands]);
|
|
73
92
|
process.exit(code);
|
|
74
93
|
}
|
|
75
94
|
|
|
@@ -107,7 +126,9 @@ async function runSelectiveLint(inputFiles) {
|
|
|
107
126
|
tasks.push(runCommand(getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]));
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
if (stylelintFiles.length > 0) {
|
|
129
|
+
if (stylelintFiles.length > 0 && !hasStylelintConfig()) {
|
|
130
|
+
console.warn('[lint] Skip stylelint: no stylelint config found');
|
|
131
|
+
} else if (stylelintFiles.length > 0) {
|
|
111
132
|
tasks.push(runCommand(getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]));
|
|
112
133
|
}
|
|
113
134
|
|