issue-map 0.2.0 → 0.3.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/README.ja.md +167 -0
- package/README.md +133 -64
- package/README.zh-CN.md +133 -0
- package/README.zh-TW.md +133 -0
- package/package.json +6 -2
- package/scripts/issue-map-i18n.ts +486 -0
- package/scripts/issue-map-model.ts +33 -13
- package/scripts/issue-map-page.ts +241 -75
- package/scripts/issue-map-serve.ts +4 -3
- package/scripts/issue-map.html +43 -12
- package/scripts/issue-map.ts +192 -61
package/scripts/issue-map.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<title
|
|
1
|
+
<title>Dev map</title>
|
|
2
2
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
3
3
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
4
4
|
<link
|
|
@@ -35,8 +35,16 @@
|
|
|
35
35
|
--t3: #8c5a24;
|
|
36
36
|
--t4: #2f7a5a;
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/*
|
|
39
|
+
* CJK 的部分只到 Noto Sans TC 為止是網路字型,簡體與日文交給系統字型(PingFang SC、
|
|
40
|
+
* Hiragino、YaHei、Meiryo)——四種語言各拉一套 Noto 會讓這頁多下載幾百 KB,而這是本機
|
|
41
|
+
* 開發用的頁面。
|
|
42
|
+
*/
|
|
43
|
+
--cjk:
|
|
44
|
+
'Noto Sans TC', 'PingFang TC', 'PingFang SC', 'Hiragino Sans', 'Microsoft JhengHei',
|
|
45
|
+
'Microsoft YaHei', Meiryo;
|
|
46
|
+
--display: 'Bricolage Grotesque', var(--cjk), sans-serif;
|
|
47
|
+
--body: 'Public Sans', var(--cjk), system-ui, sans-serif;
|
|
40
48
|
--mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
|
41
49
|
color-scheme: light;
|
|
42
50
|
}
|
|
@@ -143,6 +151,14 @@
|
|
|
143
151
|
flex-direction: column;
|
|
144
152
|
gap: 14px;
|
|
145
153
|
}
|
|
154
|
+
/* 抬頭第一行:左邊是 repo 與抓取時間,右邊是語言。 */
|
|
155
|
+
.masthead-top {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: space-between;
|
|
160
|
+
gap: 8px 16px;
|
|
161
|
+
}
|
|
146
162
|
.eyebrow {
|
|
147
163
|
font-family: var(--mono);
|
|
148
164
|
font-size: 11px;
|
|
@@ -151,6 +167,21 @@
|
|
|
151
167
|
text-transform: uppercase;
|
|
152
168
|
color: var(--faint);
|
|
153
169
|
}
|
|
170
|
+
/* 語言用原生 select:鍵盤與行動裝置都不必自己實作,而且四個選項不值得一套下拉選單。 */
|
|
171
|
+
.lang {
|
|
172
|
+
font-family: var(--mono);
|
|
173
|
+
font-size: 11.5px;
|
|
174
|
+
color: var(--muted);
|
|
175
|
+
background: var(--card);
|
|
176
|
+
border: 1px solid var(--line-strong);
|
|
177
|
+
border-radius: 2px;
|
|
178
|
+
padding: 4px 8px;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
.lang:hover {
|
|
182
|
+
color: var(--ink);
|
|
183
|
+
border-color: var(--ink);
|
|
184
|
+
}
|
|
154
185
|
h1 {
|
|
155
186
|
font-family: var(--display);
|
|
156
187
|
font-weight: 700;
|
|
@@ -844,8 +875,11 @@
|
|
|
844
875
|
|
|
845
876
|
<div class="page">
|
|
846
877
|
<header class="masthead">
|
|
847
|
-
<div class="
|
|
848
|
-
|
|
878
|
+
<div class="masthead-top">
|
|
879
|
+
<div class="eyebrow" id="eyebrow"></div>
|
|
880
|
+
<select class="lang" id="lang"></select>
|
|
881
|
+
</div>
|
|
882
|
+
<h1 id="page-title"></h1>
|
|
849
883
|
<p class="lede" id="lede"></p>
|
|
850
884
|
<div class="stats" id="stats"></div>
|
|
851
885
|
</header>
|
|
@@ -857,20 +891,17 @@
|
|
|
857
891
|
<section class="group">
|
|
858
892
|
<div class="list-head">
|
|
859
893
|
<div class="group-head">
|
|
860
|
-
<h2
|
|
894
|
+
<h2 id="list-title"></h2>
|
|
861
895
|
<span class="sub" id="list-sub"></span>
|
|
862
896
|
</div>
|
|
863
|
-
<div class="tabs" id="tabs" role="group"
|
|
897
|
+
<div class="tabs" id="tabs" role="group"></div>
|
|
864
898
|
</div>
|
|
865
899
|
<div class="rows" id="rows"></div>
|
|
866
900
|
</section>
|
|
867
901
|
|
|
868
902
|
<footer>
|
|
869
|
-
<div
|
|
870
|
-
<div>
|
|
871
|
-
重跑 <code>bun run issue-map</code> 產檔,或
|
|
872
|
-
<code>bun run issue-map:serve</code> 讓重新整理就是最新。
|
|
873
|
-
</div>
|
|
903
|
+
<div id="foot-truth"></div>
|
|
904
|
+
<div id="foot-refresh"></div>
|
|
874
905
|
<div id="foot-config"></div>
|
|
875
906
|
</footer>
|
|
876
907
|
</div>
|
package/scripts/issue-map.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* 帶進快照的 issue:所有 open issue,加上仍被 open issue 牽著的 closed issue。後者畫成「已完成」
|
|
12
12
|
* 的節點讓進度看得見,沒人牽著之後自然消失。
|
|
13
13
|
*
|
|
14
|
+
* closed 是**指名**去要的(阻擋者、parent、parent 底下的子票),不掃整包——老 repo 幾千張
|
|
15
|
+
* closed 裡通常只有個位數會留下。代價:同一組裡已完成的兄弟票要靠 GitHub 原生 sub-issue 才
|
|
16
|
+
* 抽得到,用內文 `## Parent` 慣例的 repo 看不到它們,那一組的進度會比實際少。
|
|
17
|
+
*
|
|
14
18
|
* **要畫哪個 repo**:從 cwd 的 git 推斷,不必填——在那個 repo 裡跑 `bunx issue-map@latest`
|
|
15
19
|
* 就好。要指定別的 repo 設 `GH_REPO`。標籤字彙與 parent 的慣例都能用環境變數調,見底下的
|
|
16
20
|
* `CONFIG`,整份對照表在 README。
|
|
@@ -33,6 +37,7 @@ import {
|
|
|
33
37
|
criticalPathOf,
|
|
34
38
|
groupsOf,
|
|
35
39
|
type MapIssue,
|
|
40
|
+
type NextStep,
|
|
36
41
|
type Snapshot,
|
|
37
42
|
type Status,
|
|
38
43
|
} from './issue-map-model.ts'
|
|
@@ -41,8 +46,10 @@ const TEMPLATE = new URL('./issue-map.html', import.meta.url).pathname
|
|
|
41
46
|
const CLIENT = new URL('./issue-map-page.ts', import.meta.url).pathname
|
|
42
47
|
const OUTPUT = process.argv[2] ?? 'dist/issue-map.html'
|
|
43
48
|
|
|
44
|
-
/**
|
|
49
|
+
/** 一頁的張數。GraphQL 的 `first` 最多就是 100,票再多就靠 cursor 一頁一頁接。 */
|
|
45
50
|
const PAGE = 100
|
|
51
|
+
/** 一次用 alias 指名幾張票。GraphQL 對單一查詢的節點數有上限,這個量級離它還很遠。 */
|
|
52
|
+
const BATCH = 50
|
|
46
53
|
|
|
47
54
|
function labelList(raw: string | undefined, fallback: string): readonly string[] {
|
|
48
55
|
return (raw ?? fallback)
|
|
@@ -95,50 +102,163 @@ type RawIssue = {
|
|
|
95
102
|
/** 一路帶著算好的 parent,免得同一段內文被 regex 掃好幾次。 */
|
|
96
103
|
type Issue = RawIssue & { readonly parentNumber: number | null }
|
|
97
104
|
|
|
98
|
-
interface
|
|
99
|
-
pageInfo: { hasNextPage: boolean }
|
|
100
|
-
nodes:
|
|
105
|
+
interface Page<T> {
|
|
106
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null }
|
|
107
|
+
nodes: T[]
|
|
101
108
|
}
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
|
|
110
|
+
/** 票號查不到(號碼其實是 PR,或那張票不存在)時 GraphQL 回 null。 */
|
|
111
|
+
type MaybeIssue = RawIssue | null
|
|
112
|
+
|
|
113
|
+
const ISSUE_FIELDS = `
|
|
114
|
+
number title state url body closedAt
|
|
115
|
+
author { login }
|
|
116
|
+
parent { number }
|
|
117
|
+
labels(first: 20) { nodes { name } }
|
|
118
|
+
assignees(first: 10) { nodes { login } }
|
|
119
|
+
blockedBy(first: 50) { nodes { number } }
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 跑一次 `gh api graphql`。
|
|
124
|
+
*
|
|
125
|
+
* 票號是我們自己從前一次結果拿到的整數,直接組進查詢字串;只有 cursor 走變數——它是 API 給的
|
|
126
|
+
* 不透明字串,沒有理由自己去逃脫它。
|
|
127
|
+
*/
|
|
128
|
+
function run<T>(query: string, variables: Record<string, string> = {}): T {
|
|
129
|
+
// `{owner}`/`{repo}` 由 gh 從 cwd 的 git 推斷,`GH_REPO` 可以蓋過去。
|
|
130
|
+
const args = [
|
|
131
|
+
'gh',
|
|
132
|
+
'api',
|
|
133
|
+
'graphql',
|
|
134
|
+
'-f',
|
|
135
|
+
`query=${query}`,
|
|
136
|
+
'-F',
|
|
137
|
+
'owner={owner}',
|
|
138
|
+
'-F',
|
|
139
|
+
'repo={repo}',
|
|
140
|
+
]
|
|
141
|
+
for (const [name, value] of Object.entries(variables)) args.push('-f', `${name}=${value}`)
|
|
142
|
+
const result = spawnSync(args, { stdout: 'pipe', stderr: 'pipe' })
|
|
143
|
+
if (result.exitCode !== 0) throw new Error(`gh api graphql failed: ${result.stderr.toString()}`)
|
|
144
|
+
const parsed = JSON.parse(result.stdout.toString()) as { data: T; errors?: unknown }
|
|
145
|
+
if (parsed.errors) throw new Error(`GraphQL errors: ${JSON.stringify(parsed.errors)}`)
|
|
146
|
+
return parsed.data
|
|
104
147
|
}
|
|
105
148
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
const OPEN_QUERY = `
|
|
150
|
+
query($owner: String!, $repo: String!, $after: String) {
|
|
151
|
+
repository(owner: $owner, name: $repo) {
|
|
152
|
+
nameWithOwner
|
|
153
|
+
issues(states: OPEN, first: ${PAGE}, after: $after, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
154
|
+
pageInfo { hasNextPage endCursor }
|
|
155
|
+
nodes { ${ISSUE_FIELDS} }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}`
|
|
159
|
+
|
|
160
|
+
/** open issue 全部都要,所以一路翻到底——票超過一頁不是錯誤,是常態。 */
|
|
161
|
+
function fetchOpen(): { nameWithOwner: string; open: RawIssue[] } {
|
|
162
|
+
type Result = { repository: { nameWithOwner: string; issues: Page<RawIssue> } }
|
|
163
|
+
const open: RawIssue[] = []
|
|
164
|
+
let nameWithOwner = ''
|
|
165
|
+
let after: string | null = null
|
|
166
|
+
for (;;) {
|
|
167
|
+
const variables: Record<string, string> = after ? { after } : {}
|
|
168
|
+
const { repository } = run<Result>(OPEN_QUERY, variables)
|
|
169
|
+
nameWithOwner = repository.nameWithOwner
|
|
170
|
+
open.push(...repository.issues.nodes)
|
|
171
|
+
const { hasNextPage, endCursor } = repository.issues.pageInfo
|
|
172
|
+
if (!hasNextPage || !endCursor) break
|
|
173
|
+
after = endCursor
|
|
115
174
|
}
|
|
116
|
-
|
|
175
|
+
return { nameWithOwner, open }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** alias 不能以數字開頭,所以票號前面補一個 `i`。 */
|
|
179
|
+
const alias = (number: number) => `i${number}`
|
|
117
180
|
|
|
118
|
-
|
|
119
|
-
const
|
|
181
|
+
function chunks<T>(items: readonly T[], size: number): T[][] {
|
|
182
|
+
const batches: T[][] = []
|
|
183
|
+
for (let index = 0; index < items.length; index += size) {
|
|
184
|
+
batches.push(items.slice(index, index + size))
|
|
185
|
+
}
|
|
186
|
+
return batches
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 指名要哪幾張票,一批 alias 問完。查不到的就當沒有。 */
|
|
190
|
+
function fetchByNumber(numbers: readonly number[]): RawIssue[] {
|
|
191
|
+
type Result = { repository: Record<string, MaybeIssue> }
|
|
192
|
+
const found: RawIssue[] = []
|
|
193
|
+
for (const batch of chunks(numbers, BATCH)) {
|
|
194
|
+
const query = `
|
|
120
195
|
query($owner: String!, $repo: String!) {
|
|
121
196
|
repository(owner: $owner, name: $repo) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
197
|
+
${batch.map((number) => `${alias(number)}: issue(number: ${number}) { ${ISSUE_FIELDS} }`).join('\n ')}
|
|
198
|
+
}
|
|
199
|
+
}`
|
|
200
|
+
const { repository } = run<Result>(query)
|
|
201
|
+
for (const number of batch) {
|
|
202
|
+
const issue = repository[alias(number)]
|
|
203
|
+
if (issue) found.push(issue)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return found
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function childrenQuery(parent: number): string {
|
|
210
|
+
return `
|
|
211
|
+
query($owner: String!, $repo: String!, $after: String) {
|
|
212
|
+
repository(owner: $owner, name: $repo) {
|
|
213
|
+
issue(number: ${parent}) {
|
|
214
|
+
subIssues(first: ${PAGE}, after: $after) {
|
|
215
|
+
pageInfo { hasNextPage endCursor }
|
|
216
|
+
nodes { ${ISSUE_FIELDS} }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
125
219
|
}
|
|
126
220
|
}`
|
|
221
|
+
}
|
|
127
222
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
223
|
+
/**
|
|
224
|
+
* 拿這些 parent 底下的子票,為的是把同一組裡**已完成**的兄弟票撈出來當進度。
|
|
225
|
+
*
|
|
226
|
+
* 只有 GitHub 原生 sub-issue 有值。用內文 `## Parent` 慣例的 repo 這裡是空的,那些已完成的
|
|
227
|
+
* 兄弟票就不會出現在圖上——要把它們找回來只能整包掃 closed,而那對老 repo 是幾十次請求換
|
|
228
|
+
* 幾張票。open 的兄弟不必靠這裡,它們本來就在 open 那包。
|
|
229
|
+
*/
|
|
230
|
+
function fetchChildren(parents: readonly number[]): RawIssue[] {
|
|
231
|
+
type Batch = { repository: Record<string, { subIssues: Page<RawIssue> } | null> }
|
|
232
|
+
type More = { repository: { issue: { subIssues: Page<RawIssue> } | null } }
|
|
233
|
+
const children: RawIssue[] = []
|
|
234
|
+
for (const batch of chunks(parents, BATCH)) {
|
|
235
|
+
const query = `
|
|
236
|
+
query($owner: String!, $repo: String!) {
|
|
237
|
+
repository(owner: $owner, name: $repo) {
|
|
238
|
+
${batch
|
|
239
|
+
.map(
|
|
240
|
+
(number) => `${alias(number)}: issue(number: ${number}) {
|
|
241
|
+
subIssues(first: ${PAGE}) { pageInfo { hasNextPage endCursor } nodes { ${ISSUE_FIELDS} } } }`,
|
|
242
|
+
)
|
|
243
|
+
.join('\n ')}
|
|
244
|
+
}
|
|
245
|
+
}`
|
|
246
|
+
const { repository } = run<Batch>(query)
|
|
247
|
+
for (const number of batch) {
|
|
248
|
+
const page = repository[alias(number)]?.subIssues
|
|
249
|
+
if (!page) continue
|
|
250
|
+
children.push(...page.nodes)
|
|
251
|
+
// 子票破百的 parent 很罕見,就讓它自己續抓,不為了它把整批都變成分頁查詢。
|
|
252
|
+
let after = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null
|
|
253
|
+
while (after) {
|
|
254
|
+
const next = run<More>(childrenQuery(number), { after }).repository.issue?.subIssues
|
|
255
|
+
if (!next) break
|
|
256
|
+
children.push(...next.nodes)
|
|
257
|
+
after = next.pageInfo.hasNextPage ? next.pageInfo.endCursor : null
|
|
258
|
+
}
|
|
259
|
+
}
|
|
140
260
|
}
|
|
141
|
-
return
|
|
261
|
+
return children
|
|
142
262
|
}
|
|
143
263
|
|
|
144
264
|
/** 原生 sub-issue 優先;沒有就讀內文的 `## <標題>` 之後第一個 `#<n>`。 */
|
|
@@ -149,9 +269,8 @@ function withParent(raw: RawIssue): Issue {
|
|
|
149
269
|
}
|
|
150
270
|
|
|
151
271
|
export function takeSnapshot(): Snapshot {
|
|
152
|
-
const
|
|
153
|
-
const open =
|
|
154
|
-
const closed = repository.closed.nodes.map(withParent)
|
|
272
|
+
const { nameWithOwner, open: rawOpen } = fetchOpen()
|
|
273
|
+
const open = rawOpen.map(withParent)
|
|
155
274
|
|
|
156
275
|
const openNumbers = new Set(open.map((issue) => issue.number))
|
|
157
276
|
const openParents = new Set(open.map((issue) => issue.parentNumber).filter(isNumber))
|
|
@@ -159,17 +278,17 @@ export function takeSnapshot(): Snapshot {
|
|
|
159
278
|
open.flatMap((issue) => issue.blockedBy.nodes.map((blocker) => blocker.number)),
|
|
160
279
|
)
|
|
161
280
|
|
|
162
|
-
//
|
|
163
|
-
// parent
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
]
|
|
281
|
+
// 進圖的 closed issue 只有 open issue 還牽著的那些:它們的阻擋者、它們的 parent,以及同一個
|
|
282
|
+
// parent 底下已完成的兄弟(那一組的進度)。所以不掃整包 closed,而是指名去要——老 repo 的
|
|
283
|
+
// 幾千張 closed 裡通常只有個位數會留下,翻完它們是拿幾十次請求換幾張票。
|
|
284
|
+
const referenced = [...new Set([...blockers, ...openParents])].filter(
|
|
285
|
+
(number) => !openNumbers.has(number),
|
|
286
|
+
)
|
|
287
|
+
const closed = dedupe([...fetchByNumber(referenced), ...fetchChildren([...openParents])])
|
|
288
|
+
.filter((issue) => issue.state === 'CLOSED' && !openNumbers.has(issue.number))
|
|
289
|
+
.map(withParent)
|
|
290
|
+
|
|
291
|
+
const kept = [...open, ...closed]
|
|
173
292
|
const parents = new Set(kept.map((issue) => issue.parentNumber).filter(isNumber))
|
|
174
293
|
const openChildren = new Map<number, number>()
|
|
175
294
|
for (const issue of kept) {
|
|
@@ -186,7 +305,7 @@ export function takeSnapshot(): Snapshot {
|
|
|
186
305
|
.toSorted((a, b) => a.number - b.number)
|
|
187
306
|
return {
|
|
188
307
|
generatedAt: new Date().toISOString(),
|
|
189
|
-
repo:
|
|
308
|
+
repo: nameWithOwner,
|
|
190
309
|
labels: { ready: CONFIG.ready, unready: CONFIG.unready },
|
|
191
310
|
groups: groupsOf(issues),
|
|
192
311
|
criticalPath: criticalPathOf(issues),
|
|
@@ -194,6 +313,11 @@ export function takeSnapshot(): Snapshot {
|
|
|
194
313
|
}
|
|
195
314
|
}
|
|
196
315
|
|
|
316
|
+
/** 同一張票可能同時是某人的阻擋者又是某人的兄弟,用票號收斂成一張。 */
|
|
317
|
+
function dedupe(issues: readonly RawIssue[]): RawIssue[] {
|
|
318
|
+
return [...new Map(issues.map((issue) => [issue.number, issue])).values()]
|
|
319
|
+
}
|
|
320
|
+
|
|
197
321
|
function isNumber(value: number | null): value is number {
|
|
198
322
|
return value !== null
|
|
199
323
|
}
|
|
@@ -228,16 +352,23 @@ function describeIssue(raw: Issue, context: Context): MapIssue {
|
|
|
228
352
|
return waitingFor.length ? 'blocked' : 'ready'
|
|
229
353
|
}
|
|
230
354
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (status === '
|
|
234
|
-
if (status === '
|
|
355
|
+
/** 只算出「是哪一種下一步」。句子是頁面的事,在 `issue-map-i18n.ts` 依語言組出來。 */
|
|
356
|
+
function nextStepOf(status: Status): NextStep {
|
|
357
|
+
if (status === 'done') return { kind: 'none' }
|
|
358
|
+
if (status === 'triage') return { kind: 'command', command: CONFIG.triageCommand }
|
|
359
|
+
if (status === 'active') {
|
|
360
|
+
// 沒有 assignee 但掛了 active 標籤時,能講的就只有那個標籤名。
|
|
361
|
+
const label = CONFIG.active[0]
|
|
362
|
+
return { kind: 'active', who: assignees.length ? assignees : label ? [label] : [] }
|
|
363
|
+
}
|
|
235
364
|
if (isParent) {
|
|
236
365
|
const left = context.openChildren.get(raw.number) ?? 0
|
|
237
|
-
return left ?
|
|
366
|
+
return left ? { kind: 'waitChildren', count: left } : { kind: 'parentReady' }
|
|
238
367
|
}
|
|
239
|
-
if (status === 'blocked') return
|
|
240
|
-
return has(CONFIG.human)
|
|
368
|
+
if (status === 'blocked') return { kind: 'waitIssues', issues: waitingFor }
|
|
369
|
+
return has(CONFIG.human)
|
|
370
|
+
? { kind: 'manual' }
|
|
371
|
+
: { kind: 'command', command: CONFIG.implementCommand }
|
|
241
372
|
}
|
|
242
373
|
|
|
243
374
|
const status = statusOf()
|
|
@@ -273,9 +404,9 @@ async function bundleClient(): Promise<string> {
|
|
|
273
404
|
format: 'iife',
|
|
274
405
|
minify: false,
|
|
275
406
|
})
|
|
276
|
-
if (!built.success) throw new Error(
|
|
407
|
+
if (!built.success) throw new Error(`Failed to bundle ${CLIENT}: ${built.logs.join('\n')}`)
|
|
277
408
|
const [output] = built.outputs
|
|
278
|
-
if (!output) throw new Error(
|
|
409
|
+
if (!output) throw new Error(`Bundling ${CLIENT} produced no output`)
|
|
279
410
|
return output.text()
|
|
280
411
|
}
|
|
281
412
|
|
|
@@ -299,17 +430,17 @@ export async function renderFragment(snapshot: Snapshot): Promise<string> {
|
|
|
299
430
|
|
|
300
431
|
function replaceIn(html: string, marker: RegExp, body: string, what: string): string {
|
|
301
432
|
const next = html.replace(marker, `$1${body}$2`)
|
|
302
|
-
if (next === html) throw new Error(
|
|
433
|
+
if (next === html) throw new Error(`Template is missing the ${what} block: ${TEMPLATE}`)
|
|
303
434
|
return next
|
|
304
435
|
}
|
|
305
436
|
|
|
306
437
|
export function describe(snapshot: Snapshot): string {
|
|
307
438
|
const done = snapshot.issues.filter((issue) => issue.status === 'done').length
|
|
308
|
-
return `${snapshot.issues.length - done}
|
|
439
|
+
return `${snapshot.issues.length - done} unfinished, ${done} closed but still referenced (${snapshot.generatedAt})`
|
|
309
440
|
}
|
|
310
441
|
|
|
311
442
|
if (import.meta.main) {
|
|
312
443
|
const snapshot = takeSnapshot()
|
|
313
444
|
await Bun.write(OUTPUT, await renderFragment(snapshot))
|
|
314
|
-
console.log(
|
|
445
|
+
console.log(`Wrote ${OUTPUT}: ${describe(snapshot)}`)
|
|
315
446
|
}
|