@h5l0/codelens 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ npm install -g @h5l0/codelens
23
23
  codelens
24
24
  ```
25
25
 
26
- No configuration is needed. The tool collects the data at startup and serves it to the local browser only. It does not write to the analyzed repository.
26
+ No configuration is needed. codelens collects the data at startup and writes nothing to disk by default.
27
27
 
28
28
  ## The two views
29
29
 
@@ -44,14 +44,14 @@ A treemap: each rectangle's area is its line count, its color is the category, a
44
44
  ```
45
45
  codelens [directory] [options]
46
46
 
47
- --profile <name|file> Profile, see below; built in: all, web
47
+ --profile <name|file> Profile, see below. Built in: all, web
48
48
  --config <file> Config file, defaults to <directory>/codelens.config.json
49
49
  --days <days> Calendar time span, 0 means full history (default 120)
50
50
  --exclude <glob> Extra paths to ignore, repeatable
51
51
  --port <port> Listen port, default 5178, tries the next ports when busy
52
- --host <address> Listen address, default 127.0.0.1; other addresses expose the page to your network
52
+ --host <address> Listen address, default 127.0.0.1. Other addresses expose the page to your network
53
53
  --no-open Do not open the browser automatically
54
- --no-gitignore Ignore .gitignore; only built-in heavy directories are skipped
54
+ --no-gitignore Do not filter by .gitignore. Only the built-in dependency and build directories are skipped
55
55
  --dump <dir> Write data.json and loc.json, then exit without starting a server
56
56
  --dev Dev mode with Vite hot reload
57
57
  -h, --help Show help
@@ -60,77 +60,86 @@ codelens [directory] [options]
60
60
 
61
61
  ## Profiles
62
62
 
63
- `--profile` sets how the tool splits the repository:
63
+ A profile controls how codelens divides the repository. The calendar colors commits by **group** (`groups`). The treemap colors files by **category** (`categories`).
64
64
 
65
- 1. Point it at a JSON file: `--profile ./my-profile.json`;
66
- 2. Or use a name under `profiles` in `codelens.config.json`. Use `--config` to move that file: `--profile web`.
65
+ `--profile` takes a file or a profile name:
67
66
 
68
- Two profiles are built in:
67
+ ```bash
68
+ codelens --profile ./my-profile.json # use this file as the profile
69
+ codelens --profile web # use the web profile from the config
70
+ ```
71
+
72
+ Two profiles come with codelens:
73
+
74
+ | Name | Shown in the page as | What it does |
75
+ | ------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
76
+ | `all` | All | The default profile. It has no groups and counts the whole repository. |
77
+ | `web` | Frontend / backend | Directories such as `frontend/`, `web/`, `client/` and `ui/` are frontend. The rest is backend. |
69
78
 
70
- | Name | What it does |
71
- | --- | --- |
72
- | `all` | Default; no groups, the tool counts the whole repository together |
73
- | `web` | `frontend/`, `web/`, `client/`, `ui/` and similar directories count as frontend, everything else as backend |
79
+ ### Config
74
80
 
75
- ### Config file format
81
+ The config controls the default behavior of codelens and holds a list of profiles. codelens loads `codelens.config.json` from the workspace root by default. Use `--config` to point at another file.
76
82
 
77
83
  ```jsonc
78
84
  {
79
- "profiles": {
80
- "modules": {
81
- "label": "By module",
82
- // Calendar groups: a file goes to the first group whose patterns match.
83
- // End the list with ["**"] to get an "A / everything else" split.
84
- "groups": [
85
+ "profiles": [
86
+ {
87
+ "id": "modules",
88
+ "label": "By module", // shown in the startup log
89
+ "groups": [ // calendar colors, first match wins
85
90
  { "id": "core", "label": "Core", "hue": 214, "sat": 58, "match": ["src/core/**"] },
86
91
  { "id": "web", "label": "UI", "hue": 152, "sat": 46, "match": ["src/web/**"] },
87
- { "id": "other", "label": "Other", "hue": 32, "sat": 62, "match": ["**"] }
92
+ { "id": "rest", "label": "Rest", "hue": 32, "sat": 62, "match": ["**"] }
88
93
  ],
89
- // Line-count categories: drive the legend and colors; the entry without a match is the fallback.
90
- "categories": [
94
+ "categories": [ // treemap colors and the legend
91
95
  { "id": "core", "label": "Core code", "hue": 214, "sat": 58, "match": ["src/core/**"] },
92
96
  { "id": "test", "label": "Tests", "hue": 152, "sat": 46, "defaultOn": false, "match": ["**/*.test.ts"] },
93
97
  { "id": "app", "label": "Other code", "hue": 220, "sat": 20 }
94
98
  ],
95
- // Extra paths to ignore on top of .gitignore
96
- "ignore": ["data/**", "**/*.snap"]
97
- }
98
- }
99
+ "ignore": ["data/**", "**/*.snap"] // extra paths to skip
100
+ },
101
+ ]
99
102
  }
100
103
  ```
101
104
 
102
- Field notes:
103
-
104
- - `groups[].match`, `categories[].match` and `ignore` use globs relative to the repository root: `**` crosses directories, `*` does not, `?` matches one character, and `{a,b}` matches either. A pattern without `/` matches items of that name at any depth. `/foo` is anchored to the root. `foo/` means the directory and everything inside it.
105
- - The config file accepts `//` and `/* */` comments and trailing commas.
106
- - `groups[].id` must not be `all`, and it must be unique in one profile.
107
- - `hue` and `sat` are HSL color components for the group and category colors. The defaults are 214 and 50.
108
- - `categories[].defaultOn: false` means the category is off in the legend by default. Of the built-in categories, "generated code", "docs" and "config" are off by default.
109
-
110
- Without `categories`, the tool uses six built-in categories: application code, tests, scripts, docs, config and generated code.
111
-
112
- A profile name that the config file does not define falls back to the built-in profile. A repository with a `codelens.config.json` that defines only custom profiles still supports `codelens --profile all` and `--profile web`. A name that is missing from both places is an error.
105
+ | Field | Meaning |
106
+ | ----------------------------------------------- | --------------------------------------------------------------------------------------------- |
107
+ | `profiles[*].id` | The name of the profile. This is what `--profile` takes. It must be unique in one config. |
108
+ | `profiles[*].label` | The name shown in the startup log. Without it, codelens joins the group names. |
109
+ | `profiles[*].{groups/categories}[*].id` | An identifier. It must be unique in its own list, and it must not be `all`. |
110
+ | `profiles[*].{groups/categories}[*].label` | The name shown in the page. Your own text is shown as written. |
111
+ | `profiles[*].{groups/categories}[*].match` | Path globs. An item joins the entry it matches. Required for groups, optional for categories. |
112
+ | `profiles[*].{groups/categories}[*].hue`, `sat` | The color of the entry, in HSL. The defaults are 214 and 50. |
113
+ | `profiles[*].categories[*].defaultOn` | Set it to `false` and the category is off in the legend at first. Readers can turn it on. |
114
+ | `profiles[*].ignore` | Extra paths to skip. It is merged with `--exclude`. |
115
+
116
+ Notes:
117
+
118
+ - Glob paths are relative to the repository root. `**` crosses directories. `*` and `?` stay within one segment. `{a,b}` matches either one.
119
+ - A pattern without `/` matches that name at any depth. `/foo` counts from the repository root. `foo/` means the directory and everything inside it.
120
+ - The config accepts `//` and `/* */` comments and trailing commas.
121
+ - Without `categories`, codelens uses six built-in ones. They are application code, tests, scripts, docs, config and generated code. Of these, "docs", "config" and "generated code" are off at first.
122
+ - A profile name that the config does not define falls back to a built-in profile. If neither has the name, codelens reports an error.
113
123
 
114
124
  ## Counting rules
115
125
 
116
- - The tool honors `.gitignore` by default. If the directory is not a git repository, the tool applies equivalent ignore rules instead. `--no-gitignore` turns this off.
117
- - The tool always skips dependency and build directories, such as `node_modules`, `dist`, `build`, `coverage`, `.venv`, `__pycache__` and `target`.
118
- - `--exclude` and the config's `ignore` apply to both views. The tool does not count excluded directories, and the calendar does not show them.
119
- - The tool skips binary files, files larger than 3MB and empty files. If the tool cannot read a file, it skips that file and reports the count in the startup log. Line counts are physical lines; the newline at the end of a file does not count as one more line.
120
- - The calendar puts each commit in the day of its commit time (committer date). This matches the `--days` filter. "Changed lines" means insertions plus deletions.
121
- - Merge commits, empty commits and commits that only change file modes or binaries have no line counts. They still appear in the commit list and count as commits.
122
- - If you run the tool on a subdirectory of a repository, both views count that subdirectory only and resolve paths relative to it.
126
+ - codelens filters by `.gitignore` by default. If the directory is not a git repository, codelens applies equal ignore rules. `--no-gitignore` turns the filter off.
127
+ - codelens always skips dependency and build directories such as `node_modules`, `dist`, `build`, `coverage`, `.venv`, `__pycache__` and `target`.
128
+ - `--exclude` and the config's `ignore` affect both views. An excluded directory has no line counts and does not appear in the calendar.
129
+ - codelens skips binary files, files larger than 3MB and empty files. It skips a file it cannot read and reports the number in the startup log. Line counts are physical lines. The newline at the end of a file does not add a line.
130
+ - The calendar puts each commit in the day of its commit time (committer date). `--days` filters by the same time. "Changed lines" means insertions plus deletions.
131
+ - Merge commits, empty commits and commits that only change file modes or binaries have no line counts. They still appear in the commit list, and they count as commits.
132
+ - When you run codelens on a subdirectory of a repository, both views count that subdirectory only and resolve paths relative to it.
123
133
 
124
134
  ## Known limits
125
135
 
126
- - The change calendar needs git. A repository without commits, or without git, leaves the calendar empty. The lines view still works.
136
+ - The change calendar needs git. Without commits or without git, the calendar is empty. The lines view still works.
137
+ - The treemap draws at most 6000 rectangles. It does not show the rest.
127
138
  - Line counts say nothing about code complexity.
128
- - The treemap draws at most 6000 rectangles. The rest are not shown.
129
- - The calendar is padded to whole weeks. A window that does not start or end on a week boundary shows a few extra days without data, with a paler background.
130
139
 
131
140
  ## Languages
132
141
 
133
- The page follows the browser language. Simplified Chinese, English, Japanese and Korean are built in. Other languages fall back to English. Append `?lang={langCode}` to the URL to override the language, for example `?lang=zh` or `?lang=ko`.
142
+ The page follows the browser language. Simplified Chinese, English, Japanese and Korean are built in. English is the default. Append `?lang={langCode}` to the URL to override the language, for example `?lang=zh` or `?lang=ko`.
134
143
 
135
144
  ## Development
136
145
 
package/README.zh.md CHANGED
@@ -23,7 +23,7 @@ npm install -g @h5l0/codelens
23
23
  codelens
24
24
  ```
25
25
 
26
- 无需配置。数据在启动时生成,只提供给本机浏览器,不写入被统计的仓库。
26
+ 无需配置。启动时生成数据,默认不写入硬盘。
27
27
 
28
28
  ## 两个视图
29
29
 
@@ -44,93 +44,103 @@ codelens
44
44
  ```
45
45
  codelens [目录] [选项]
46
46
 
47
- --profile <名称|文件> 配置档,见下节,内置 all、web
48
- --config <文件> 配置文件,默认 <目录>/codelens.config.json
47
+ --profile <名称|文件> 预设,见下节,内置 all、web
48
+ --config <文件> 配置,默认 <目录>/codelens.config.json
49
49
  --days <天数> 日历时间跨度,0 表示全部历史(默认 120)
50
50
  --exclude <glob> 额外忽略的路径,可重复
51
51
  --port <端口> 监听端口,默认 5178,被占用时向后尝试
52
- --host <地址> 监听地址,默认 127.0.0.1;监听其他地址时页面对同网段可见
52
+ --host <地址> 监听地址,默认 127.0.0.1。监听其他地址时,页面对同网段可见
53
53
  --no-open 不自动打开浏览器
54
- --no-gitignore 不按 .gitignore 过滤,只跳过内置的重目录
54
+ --no-gitignore 不按 .gitignore 过滤,只跳过内置的依赖与构建目录
55
55
  --dump <目录> 只写出 data.json 与 loc.json 后退出,不启动服务
56
56
  --dev 开发模式,用 Vite 托管前端源码并热更新
57
57
  -h, --help 显示帮助
58
58
  -v, --version 显示版本
59
59
  ```
60
60
 
61
- ## 配置档
61
+ ## 预设(profile)
62
62
 
63
- `--profile` 决定切分仓库的维度:
63
+ 预设用于控制统计的划分方式。改动日历按**分组**(`groups`)给提交上色。行数视图按**分类**(`categories`)给文件上色。
64
64
 
65
- 1. 指向 json 文件:`--profile ./my-profile.json`;
66
- 2. 取 `codelens.config.json` 里 `profiles` 下的档名,位置可用 `--config` 改:`--profile web`。
65
+ `--profile` 后面可以写文件,也可以写预设名:
67
66
 
68
- 内置两档:
67
+ ```bash
68
+ codelens --profile ./my-profile.json # 用这个文件当预设
69
+ codelens --profile web # 用配置里的 web 预设
70
+ ```
71
+
72
+ codelens 自带两个预设:
69
73
 
70
- | 名称 | 作用 |
71
- | --- | --- |
72
- | `all` | 默认档,不分组,整个仓库一起统计 |
73
- | `web` | `frontend/`、`web/`、`client/`、`ui/` 等算前端,其余算后端 |
74
+ | 名称 | 页面上显示为 | 作用 |
75
+ | ------- | -------------- | ---------------------------------------------------------------- |
76
+ | `all` | 全部 | 默认预设,不分组,统计整个仓库。 |
77
+ | `web` | 前后端 | `frontend/`、`web/`、`client/`、`ui/` 算前端。其余目录算后端。 |
74
78
 
75
- ### 配置文件格式
79
+ ### 配置
80
+
81
+ 配置用于控制程序默认行为,可包含一组预设。
82
+ 默认加载工作区根目录的配置文件:`codelens.config.json`,用 `--config` 可以指定别的文件。
76
83
 
77
84
  ```jsonc
78
85
  {
79
- "profiles": {
80
- "modules": {
81
- "label": "按模块",
82
- // 改动日历的分组:命中的文件算进该组,按数组顺序取第一个命中的。
83
- // 最后一个用 ["**"] 兜底,就能得到「A / 其余」这种两分效果。
84
- "groups": [
86
+ "profiles": [
87
+ {
88
+ "id": "modules",
89
+ "label": "按模块", // 显示在启动日志里
90
+ "groups": [ // 改动日历的分组配色,取第一个命中的
85
91
  { "id": "core", "label": "核心", "hue": 214, "sat": 58, "match": ["src/core/**"] },
86
92
  { "id": "web", "label": "界面", "hue": 152, "sat": 46, "match": ["src/web/**"] },
87
- { "id": "other", "label": "其他", "hue": 32, "sat": 62, "match": ["**"] }
93
+ { "id": "rest", "label": "其余", "hue": 32, "sat": 62, "match": ["**"] }
88
94
  ],
89
- // 行数视图的分类:决定图例与配色,省略 match 的那一项是兜底类。
90
- "categories": [
95
+ "categories": [ // 行数视图的分类配色与图例
91
96
  { "id": "core", "label": "核心代码", "hue": 214, "sat": 58, "match": ["src/core/**"] },
92
97
  { "id": "test", "label": "测试", "hue": 152, "sat": 46, "defaultOn": false, "match": ["**/*.test.ts"] },
93
98
  { "id": "app", "label": "其他代码", "hue": 220, "sat": 20 }
94
99
  ],
95
- // .gitignore 之外额外忽略的路径
96
- "ignore": ["data/**", "**/*.snap"]
97
- }
98
- }
100
+ "ignore": ["data/**", "**/*.snap"] // 额外跳过的路径
101
+ },
102
+ ]
99
103
  }
100
104
  ```
101
105
 
102
- 字段说明:
103
-
104
- - `groups[].match`、`categories[].match`、`ignore` 使用仓库相对路径的 glob:`**` 跨目录,`*` 不跨目录,`?` 匹配单个字符,`{a,b}` 择一;不含 `/` 的模式匹配任意层级的同名项,`/foo` 从仓库根起算,`foo/` 表示目录及其全部内容。
105
- - 配置文件允许 `//`、`/* */` 注释与尾随逗号。
106
- - `groups[].id` 不能是保留的 `all`,同一档里不能重复。
107
- - `hue`、`sat` 是 HSL 颜色分量,用于分组色与分类色,缺省为 214、50。
108
- - `categories[].defaultOn` `false` 表示该分类在页面图例里默认关闭;内置分类中「生成代码」「文档」「配置」默认关闭。
109
-
110
- 不写 `categories` 时使用内置六类:应用代码、测试、脚本、文档、配置、生成代码。
111
-
112
- 配置里没有的档名回退到内置档:仓库里放一份只定义自定义档的 `codelens.config.json`,`--profile all`、`--profile web` 依然可用。两边都没有的档名才报错。
106
+ | 字段 | 作用 |
107
+ | ----------------------------------------------- | ----------------------------------------------------------------- |
108
+ | `profiles[*].id` | 预设名,`--profile` 指定名称。同一份配置里不能重复。 |
109
+ | `profiles[*].label` | 启动日志里显示的预设名。不写时用各分组名拼出来。 |
110
+ | `profiles[*].{groups/categories}[*].id` | 标识。同一份列表里不能重复,也不能叫 `all`。 |
111
+ | `profiles[*].{groups/categories}[*].label` | 页面上显示的名字。你写的名字会原样显示。 |
112
+ | `profiles[*].{groups/categories}[*].match` | 路径 glob。文件命中哪一条,就归入那一项。分组必填,分类可以省略。 |
113
+ | `profiles[*].{groups/categories}[*].hue`、`sat` | 这一项的颜色,用 HSL 表示。默认 214 和 50。 |
114
+ | `profiles[*].categories[*].defaultOn` | 设为 `false` 时,这个分类在图例里默认关闭。读者可以自己打开。 |
115
+ | `profiles[*].ignore` | 额外跳过的路径。它会和 `--exclude` 合并。 |
116
+
117
+ 说明:
118
+
119
+ - glob 的路径相对仓库根目录。`**` 跨目录。`*` 和 `?` 只在单段里匹配。`{a,b}` 表示任选一个。
120
+ - 不含 `/` 的模式匹配任意层级的同名项。`/foo` 从仓库根算起。`foo/` 表示目录及其全部内容。
121
+ - 配置里可以写 `//`、`/* */` 注释和尾随逗号。
122
+ - 不写 `categories` 时,codelens 用内置的六类。它们是应用代码、测试、脚本、文档、配置、生成代码。其中「文档」「配置」「生成代码」默认关闭。
123
+ - 配置里没有的预设名会回退到内置预设。两边都没有就报错。
113
124
 
114
125
  ## 统计规则
115
126
 
116
- - 默认遵守仓库的 `.gitignore`;目录不是 git 仓库时改用等价的忽略规则。`--no-gitignore` 关闭该过滤。
117
- - 始终跳过 `node_modules`、`dist`、`build`、`coverage`、`.venv`、`__pycache__`、`target` 等依赖与构建目录。
118
- - `--exclude` 与配置里的 `ignore` 对两个视图同时生效:被排除的目录既不统计行数,也不计入改动日历。
119
- - 二进制文件、超过 3MB 的文件、空文件不统计;读不出的文件只跳过它,并在启动日志里给出数量。行数为物理行数,文件末尾换行不计一行。
120
- - 改动日历按提交时间(committer date)归入所在天,与 `--days` 的过滤规则一致;改动行数 = 新增 + 删除。
121
- - 合并提交、空提交、只改权限或只动二进制的提交没有行数,但仍出现在提交列表里并计入提交数。
122
- - 对仓库的子目录运行时,两个视图都只统计该子目录,路径也相对它计算。
127
+ - 默认按仓库的 `.gitignore` 过滤。目录不是 git 仓库时,codelens 改用等价的忽略规则。`--no-gitignore` 可以关掉这个过滤。
128
+ - codelens 始终跳过 `node_modules`、`dist`、`build`、`coverage`、`.venv`、`__pycache__`、`target` 这类依赖和构建目录。
129
+ - `--exclude` 和配置里的 `ignore` 对两个视图都生效。被排除的目录不统计行数,也不出现在改动日历里。
130
+ - 二进制文件、超过 3MB 的文件、空文件都不统计。读不出的文件会被跳过,数量写在启动日志里。行数按物理行数算,文件末尾的换行不算一行。
131
+ - 改动日历按提交时间(committer date)把提交放到当天。`--days` 用的是同一个时间。改动行数是新增行数加删除行数。
132
+ - 合并提交、空提交、只改权限的提交、只改二进制的提交都没有行数。它们仍然出现在提交列表里,也计入提交数。
133
+ - 在仓库的子目录里运行时,两个视图只统计这个子目录,路径也相对它计算。
123
134
 
124
135
  ## 已知限制
125
136
 
126
137
  - 改动日历依赖 git:没有提交或没有 git 时日历为空,行数视图仍可用。
127
- - 行数不代表代码复杂度。
128
138
  - 树形图一次最多绘制 6000 个方块,超出部分不显示。
129
- - 日历按整周补齐,窗口首尾不足一周时会多画出几天没有数据的格子(底色更白)。
139
+ - 行数不代表代码复杂度。
130
140
 
131
141
  ## 多语言
132
142
 
133
- 页面语言跟随浏览器,内置简体中文、English、日本語、한국어,其余回落到英文。地址后加 `?lang={langCode}` 可临时覆盖,例如 `?lang=en`。
143
+ 页面语言跟随浏览器,内置简体中文、English、日本語、한국어,默认英文。地址后加 `?lang={langCode}` 可临时覆盖,例如 `?lang=en`。
134
144
 
135
145
  ## 开发
136
146
 
package/dist/cli/index.js CHANGED
@@ -102,19 +102,23 @@ async function main() {
102
102
  });
103
103
  console.log(`codelens ${version()}`);
104
104
  log('repo', root);
105
- log('profile', `${profile.name} (${profile.label})${profile.configPath ? ` · ${profile.configPath}` : ''}`);
105
+ log('profile', `${profile.name} (${profile.label})`);
106
+ if (profile.configPath) {
107
+ log('config', profile.configPath);
108
+ }
106
109
  const loc = await readLoc(root, profile, args.useGitignore);
107
110
  const scanLabel = loc.scan.mode === 'git' ? 'git index, filtered by .gitignore' : 'directory walk';
108
- log('files', `${f(loc.data.totals.files)} files · ${f(loc.data.totals.lines)} lines (${scanLabel})`);
111
+ log('files', `${f(loc.data.totals.files)} files, ${f(loc.data.totals.lines)} lines (${scanLabel})`);
109
112
  if (loc.data.skipped.unreadable > 0) {
110
113
  log('warn', `${f(loc.data.skipped.unreadable)} files could not be read and were skipped`);
111
114
  }
112
- if (profile.groups.length > 0) {
113
- log('groups', profile.groups.map((group) => `${group.label}=${group.match.join(' ')}`).join(' '));
114
- }
115
+ // 分组一行一个,规则长了也不会挤成一条
116
+ profile.groups.forEach((group, index) => {
117
+ log(index === 0 ? 'groups' : '', `${group.label}=${group.match.join(' ')}`);
118
+ });
115
119
  const calendar = await readCalendar(root, profile, args.days);
116
120
  log('calendar', calendar.range.min
117
- ? `${calendar.range.min} ~ ${calendar.range.max} · ${calendar.totals.days} days with commits`
121
+ ? `${calendar.range.min} ~ ${calendar.range.max} (${calendar.totals.days} days with commits)`
118
122
  : 'no commits in this range');
119
123
  if (args.dump) {
120
124
  const outDir = resolve(args.dump);
@@ -1,7 +1,8 @@
1
1
  // ---------------------------------------------------------------------------
2
- // 配置档
3
- // --profile 选择一份配置:内置的 all(不区分)与 web(常见前后端目录),
4
- // 或在 codelens.config.json(也可用 --profile 直接指向 json 文件)里自定义,
2
+ // 预设
3
+ // --profile 选择一份预设:内置的 all(不区分)与 web(常见前后端目录),
4
+ // 或在配置 codelens.config.json(也可用 --profile 直接指向 json 文件)里自定义。
5
+ // 配置的 profiles 是数组,每项带 id,它就是 --profile 后面写的名字。
5
6
  // 用 groups 划分前后端等分组,用 categories 划分模块与文件类别。
6
7
  // 内置项的 label 是英文,labelKey 供页面按语言覆盖;用户配置的 label 原样使用。
7
8
  // ---------------------------------------------------------------------------
@@ -78,9 +79,9 @@ export const DEFAULT_CATEGORIES = [
78
79
  { id: 'app', label: 'Application code', labelKey: 'category.app', hue: 214, sat: 58 },
79
80
  ];
80
81
  // ---------------------------------------------------------------------------
81
- // 内置配置档
82
+ // 内置预设
82
83
  // ---------------------------------------------------------------------------
83
- /** 未配置的兜底档:不做任何分组,整个仓库一起统计。 */
84
+ /** 未配置的兜底预设:不做任何分组,整个仓库一起统计。 */
84
85
  const PROFILE_ALL = { label: 'All', labelKey: 'profile.all' };
85
86
  /** 常见前后端目录布局:前端目录归前端,其余归后端。 */
86
87
  const PROFILE_WEB = {
@@ -208,6 +209,25 @@ function parseEntry(value, where) {
208
209
  }
209
210
  return entry;
210
211
  }
212
+ function parseProfiles(value, where) {
213
+ if (value === undefined) {
214
+ return [];
215
+ }
216
+ if (!Array.isArray(value)) {
217
+ fail(where, 'must be an array');
218
+ }
219
+ const seen = new Set();
220
+ return value.map((item, i) => {
221
+ const at = `${where}[${i}]`;
222
+ const obj = asObject(item, at);
223
+ const id = asString(obj.id, `${at}.id`);
224
+ if (seen.has(id)) {
225
+ fail(where, `contain a duplicate id: ${id}`);
226
+ }
227
+ seen.add(id);
228
+ return { id, entry: parseEntry(obj, at) };
229
+ });
230
+ }
211
231
  function readJson(path) {
212
232
  let text;
213
233
  try {
@@ -279,10 +299,23 @@ function stripJsonc(text) {
279
299
  }
280
300
  return out;
281
301
  }
302
+ /** 同一份列表里的 id 必须唯一,也不能占用保留名。 */
303
+ function checkIds(defs, where) {
304
+ const seen = new Set();
305
+ for (const def of defs) {
306
+ if (seen.has(def.id)) {
307
+ fail(where, `contain a duplicate id: ${def.id}`);
308
+ }
309
+ if (RESERVED_IDS.has(def.id)) {
310
+ fail(where, `must not use the reserved id: ${def.id}`);
311
+ }
312
+ seen.add(def.id);
313
+ }
314
+ }
282
315
  /**
283
- * 按名字解析配置档:
284
- * 1. 名字指向 json 文件时直接当档用;
285
- * 2. 否则先找配置文件里的 profiles[名字],再找内置档。
316
+ * 按名字解析预设:
317
+ * 1. 名字指向 json 文件时直接当预设用;
318
+ * 2. 否则先找配置的 profiles 里 id 同名的一项,再找内置预设。
286
319
  */
287
320
  export function loadProfile(opts) {
288
321
  const { root, name } = opts;
@@ -298,13 +331,14 @@ export function loadProfile(opts) {
298
331
  }
299
332
  else if (opts.configPath || exists(configPath)) {
300
333
  const file = asObject(readJson(configPath), configPath);
301
- const profiles = file.profiles === undefined ? {} : asObject(file.profiles, `profiles of ${configPath}`);
302
- if (hasOwn(profiles, name)) {
303
- entry = parseEntry(profiles[name], `profiles.${name} of ${configPath}`);
334
+ const profiles = parseProfiles(file.profiles, `profiles of ${configPath}`);
335
+ const found = profiles.find((item) => item.id === name);
336
+ if (found) {
337
+ entry = found.entry;
304
338
  usedConfig = configPath;
305
339
  }
306
340
  else if (hasOwn(BUILTIN_PROFILES, name)) {
307
- // 配置里没有这一档就回退内置档:仓库里放了一份自定义配置,
341
+ // 配置里没有这个预设就回退内置预设:仓库里放了一份自定义配置,
308
342
  // 不该让默认的 `codelens` / `--profile web` 直接跑不起来
309
343
  entry = BUILTIN_PROFILES[name];
310
344
  }
@@ -316,35 +350,24 @@ export function loadProfile(opts) {
316
350
  entry = BUILTIN_PROFILES[name];
317
351
  }
318
352
  else {
319
- throw new Error(`no profile named ${name}; available: ${names(undefined, BUILTIN_PROFILES)}`);
353
+ throw new Error(`no profile named ${name}; available: ${names([], BUILTIN_PROFILES)}`);
320
354
  }
321
355
  const groups = entry.groups ?? [];
322
- const ids = new Set();
323
- for (const group of groups) {
324
- if (ids.has(group.id)) {
325
- fail(`groups of ${usedConfig ?? name}`, `contain a duplicate id: ${group.id}`);
326
- }
327
- if (RESERVED_IDS.has(group.id)) {
328
- fail(`groups of ${usedConfig ?? name}`, `must not use the reserved id: ${group.id}`);
329
- }
330
- ids.add(group.id);
331
- }
332
- for (const category of entry.categories ?? []) {
333
- if (RESERVED_IDS.has(category.id)) {
334
- fail(`categories of ${usedConfig ?? name}`, `must not use the reserved id: ${category.id}`);
335
- }
336
- }
356
+ const categories = entry.categories ?? DEFAULT_CATEGORIES;
357
+ // 分组与分类各用一套 id:一个给日历配色,一个给树形图配色,两边重名不算冲突
358
+ checkIds(groups, `groups of ${usedConfig ?? name}`);
359
+ checkIds(categories, `categories of ${usedConfig ?? name}`);
337
360
  return {
338
361
  name,
339
362
  label: entry.label ?? (groups.length > 0 ? groups.map((group) => group.label).join(' / ') : 'All'),
340
363
  groups,
341
- categories: entry.categories ?? DEFAULT_CATEGORIES,
364
+ categories,
342
365
  ignore: [...(entry.ignore ?? []), ...opts.exclude],
343
366
  configPath: usedConfig,
344
367
  };
345
368
  }
346
369
  function names(profiles, builtin) {
347
- return [...Object.keys(profiles ?? {}), ...Object.keys(builtin)].join(', ');
370
+ return [...profiles.map((item) => item.id), ...Object.keys(builtin)].join(', ');
348
371
  }
349
372
  function isProfileFile(name) {
350
373
  return name.endsWith('.json') || name.includes('/') || name.includes('\\');
@@ -357,7 +380,7 @@ function exists(path) {
357
380
  return false;
358
381
  }
359
382
  }
360
- /** 供帮助信息使用:列出内置配置档。 */
383
+ /** 供帮助信息使用:列出内置预设。 */
361
384
  export function builtinProfileNames() {
362
385
  return Object.keys(BUILTIN_PROFILES);
363
386
  }