@pghoya2956/livemap 2.0.0 → 2.0.1
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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/adapters/router.mjs +29 -6
- package/src/cli.mjs +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
버전마다 `## [X.Y.Z] - YYYY-MM-DD` 절을 둔다. 릴리스 워크플로가 태그 버전의 절이 있는지 확인한다.
|
|
4
4
|
|
|
5
|
+
## [2.0.1] - 2026-09-19
|
|
6
|
+
|
|
7
|
+
### 고친 것
|
|
8
|
+
|
|
9
|
+
- 레이아웃 라우트로 얹힌 셸이 부르는 API가 자식 화면에 잡힌다. 그전에는 `<Route>`가 자식을 감싸면 그 셸 컴포넌트를 아예 분류하지 않아서, 셸에만 있는 호출(로그아웃 같은 것)이 어느 화면에도 붙지 않고 「어느 화면도 부르지 않는 API」 경고로 남았다. 셸은 자식 화면마다 함께 그려지므로 그 호출은 그 화면에서 실제로 일어난다. 실측으로 소비 프로젝트의 경고가 3에서 2로 줄었다. 합치는 것은 API 리터럴과 hook뿐이고, 자료 출처(`source`·`mockVia`·`files`)는 그 화면이 제 자료를 어디서 받는지를 말하는 값이라 합치지 않는다. 레이아웃 셸은 `pagesDir` 밖에 있는 경우가 많아 `localDirs`까지 찾는다(이름이 겹치면 `pagesDir` 쪽이 이긴다).
|
|
10
|
+
- 하위 명령에 붙인 `--help`·`-h`가 도움말을 내고 명령을 실행하지 않는다. 그전에는 `--help`를 첫 인자일 때만 도움말로 읽어서 `livemap init --help`가 도움말 대신 `init`을 실행했다. 빈 폴더에서 물으면 `map/` 뼈대와 `.gitignore` 줄을 만들고 종료 코드 0으로 끝났다. 값을 뒤에 받는 플래그의 값 자리(`--root --help` 등)는 그대로 값으로 읽는다.
|
|
11
|
+
|
|
12
|
+
### 추가
|
|
13
|
+
|
|
14
|
+
- 팩 내용 계약 검사(`test/pack-contents.test.mjs`): 소비 프로젝트가 쓰는 것(실행기·엔진·화면·예산 설정·템플릿·문서·공개 리포터 경로)이 팩에 있고 저장소 개발 도구(`scripts/`)가 새어 들어가지 않는지 본다. 문서가 소비자용이라고 적은 것과 실제 팩이 어긋나던 자리를 검사로 막는다.
|
|
15
|
+
|
|
16
|
+
### 값이 바뀌는 것
|
|
17
|
+
|
|
18
|
+
- 레이아웃 라우트를 쓰는 프로젝트에서 셸이 부르는 API가 이제 자식 화면의 `calls` 엣지로 잡힌다. 그만큼 「어느 화면도 부르지 않는 API」 경고가 줄고, 화면의 `apis` 읽기 상태가 `partial`에서 `rule`로 바뀔 수 있다. 실측으로 소비 프로젝트의 `check` 경고가 3에서 2로 줄었다. **경고 수를 값으로 고정해 둔 검사가 있으면 그 기대값을 낮춰야 한다.** 필드 모양은 그대로다.
|
|
19
|
+
|
|
20
|
+
### 고침(문서)
|
|
21
|
+
|
|
22
|
+
- 1.3.0 절의 클릭 대상 크롤러 항목은 소비 프로젝트용 변경이 아니다. 크롤러(`scripts/route-crawl.mjs`)는 npm 팩에 들어가지 않는 엔진 저장소 개발 도구이고 `scripts/smoke.sh`·`scripts/leak-scan.sh`와 같은 부류다. 팩에 들어가는 것은 `package.json`의 `files`가 정한 일곱(`bin/`·`src/`·`site/`·`budget/`·`templates/`·`docs/`·`CHANGELOG.md`)뿐이다. 소비 프로젝트가 돌리는 화면 검사는 팩에 든 `budget/`이다.
|
|
23
|
+
|
|
5
24
|
## [2.0.0] - 2026-09-19
|
|
6
25
|
|
|
7
26
|
여정 정본을 역할별 마크다운 디렉터리에서 읽고, 정본 자체의 어긋남과 정본↔검사 양방향을 검사한다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pghoya2956/livemap",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Project status board engine: scans a repository into a graph and serves a one-screen map of journeys, screens, APIs, tests and work.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/adapters/router.mjs
CHANGED
|
@@ -12,6 +12,17 @@ export default function router(g, fs, cfg) {
|
|
|
12
12
|
const owner = new Map();
|
|
13
13
|
for (const f of pageFiles) for (const m of fs.read(f).matchAll(/export\s+(?:function|const)\s+([A-Z]\w+)/g)) owner.set(m[1], f);
|
|
14
14
|
|
|
15
|
+
// 레이아웃 셸은 pagesDir 밖(components 등)에 있는 경우가 많다. 화면 소유자 표는 그대로 두고 셸을 찾을 때만 localDirs까지 본다.
|
|
16
|
+
// 이름이 겹치면 pagesDir 쪽이 이긴다 — 화면 파일 결정은 바꾸지 않는다
|
|
17
|
+
const shellOwner = new Map();
|
|
18
|
+
for (const dir of c.localDirs || []) {
|
|
19
|
+
if (dir === c.pagesDir) continue;
|
|
20
|
+
for (const f of fs.walk(dir, (p) => /\.tsx?$/.test(p))) {
|
|
21
|
+
for (const m of fs.read(f).matchAll(/export\s+(?:function|const)\s+([A-Z]\w+)/g)) if (!shellOwner.has(m[1])) shellOwner.set(m[1], f);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const shellFile = (name) => owner.get(name) || shellOwner.get(name) || null;
|
|
25
|
+
|
|
15
26
|
const hookApi = c.hookApi || null;
|
|
16
27
|
const appDir = appDirOf(c.app);
|
|
17
28
|
const classify = (file) => {
|
|
@@ -46,24 +57,36 @@ export default function router(g, fs, cfg) {
|
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
const app = fs.read(c.app);
|
|
49
|
-
let parent = null, n = 0;
|
|
60
|
+
let parent = null, shell = null, n = 0;
|
|
50
61
|
app.split('\n').forEach((line, i) => {
|
|
51
62
|
const m = line.match(/<Route\s+(?:path="([^"]+)"|(index))[^>]*element=\{(?:\w*[gG]uard\()?<(\w+)/);
|
|
52
63
|
if (m) {
|
|
53
64
|
const own = m[2] ? '' : m[1];
|
|
54
65
|
const path = parent ? (own ? `${parent}/${own}` : parent) : own;
|
|
55
66
|
const isLayout = !/\/>\s*$/.test(line.trim()) && !/<\/Route>/.test(line);
|
|
56
|
-
|
|
67
|
+
// 레이아웃 라우트는 사람이 가는 화면이 아니라 자식 화면마다 함께 그려지는 셸이다.
|
|
68
|
+
// 그래서 화면 노드를 만들지 않되, 셸이 부르는 API는 자식 화면에서 실제로 일어나므로 리터럴과 hook을 자식에 합친다.
|
|
69
|
+
if (isLayout) {
|
|
70
|
+
parent = m[1];
|
|
71
|
+
const sf = shellFile(m[3]);
|
|
72
|
+
shell = sf ? classify(sf) : null;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
57
75
|
if (path === '*' || path.endsWith('/*')) return;
|
|
58
76
|
const file = owner.get(m[3]) || null;
|
|
59
77
|
const cls = file ? classify(file) : { files: [], mockVia: [], fixedVia: [], source: 'static', hooks: [], apiLiterals: [] };
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
// 셸의 리터럴·hook을 자식 화면에 더한다. 같은 파일·줄·경로는 한 번만 넣는다.
|
|
79
|
+
// source·mockVia·files는 합치지 않는다 — 그건 그 화면이 제 자료를 어디서 받는지를 말하는 값이고 셸은 자료를 주지 않는다
|
|
80
|
+
const seenLit = new Set(cls.apiLiterals.map((l) => `${l.file}:${l.line}:${l.path}`));
|
|
81
|
+
const apiLiterals = [...cls.apiLiterals, ...(shell?.apiLiterals ?? []).filter((l) => !seenLit.has(`${l.file}:${l.line}:${l.path}`))];
|
|
82
|
+
const hooks = [...new Set([...cls.hooks, ...(shell?.hooks ?? [])])];
|
|
83
|
+
const props = { component: m[3], file, guarded: /[gG]uard\(/.test(line) || parent !== null, source: cls.source, mockVia: cls.mockVia, fixedVia: cls.fixedVia, files: cls.files, last: file ? fs.lastCommit(file) : null, apiLiterals };
|
|
84
|
+
if (hookApi) props.hookApiKeys = hooks;
|
|
62
85
|
g.add('screen', path, path, props, { file: c.app, line: i + 1, rule: 'router:<Route path>' });
|
|
63
|
-
for (const h of
|
|
86
|
+
for (const h of hooks) { const a = hookApi[h]; g.add('api', a, a); g.link('screen', path, 'calls', 'api', a); }
|
|
64
87
|
n += 1;
|
|
65
88
|
}
|
|
66
|
-
if (/<\/Route>/.test(line)) parent = null;
|
|
89
|
+
if (/<\/Route>/.test(line)) { parent = null; shell = null; }
|
|
67
90
|
});
|
|
68
91
|
return n === 0 ? '라우트 0건' : null;
|
|
69
92
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -114,6 +114,9 @@ const USAGE = `usage: livemap <command>
|
|
|
114
114
|
test-report --import <파일> [--sha <커밋>] Playwright JSON·JUnit·livemap 리포터 출력을 결과 JSON에
|
|
115
115
|
--version 엔진 버전`;
|
|
116
116
|
|
|
117
|
+
// 값을 뒤에 받는 플래그. 그 값 자리에 온 --help 는 도움말 요청이 아니다
|
|
118
|
+
const VALUE_FLAGS = new Set(['--root', '--out', '--semantic', '--port', '--static', '--base', '--import', '--sha']);
|
|
119
|
+
|
|
117
120
|
function readConfig(root) {
|
|
118
121
|
const file = resolve(root, CONFIG);
|
|
119
122
|
if (!existsSync(file)) return { error: `${CONFIG} 없음: 프로젝트 루트에서 실행하거나 먼저 livemap init` };
|
|
@@ -157,7 +160,9 @@ export async function main(argv = []) {
|
|
|
157
160
|
const cmd = argv[0] || 'build';
|
|
158
161
|
const opt = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; };
|
|
159
162
|
if (cmd === '--version' || cmd === '-v' || cmd === 'version') { console.log(VERSION); return 0; }
|
|
160
|
-
|
|
163
|
+
// 하위 명령 뒤에 붙인 --help·-h 도 도움말이다. 값이 필요한 플래그의 값 자리는 빼서 --out --help 같은 경우를 건드리지 않는다.
|
|
164
|
+
// 이 줄이 없으면 도움말을 물은 사람이 명령을 실행당한다. init 은 파일을 쓰므로 그 피해가 실제였다.
|
|
165
|
+
if (cmd === 'help' || argv.some((a, i) => (a === '--help' || a === '-h') && !VALUE_FLAGS.has(argv[i - 1]))) { console.log(USAGE); return 0; }
|
|
161
166
|
if (cmd === 'init') { const { init } = await import('./init.mjs'); return init({ root: process.cwd(), pkgRoot: PKG_ROOT }); }
|
|
162
167
|
|
|
163
168
|
const root = resolve(opt('root', process.cwd()));
|